Files
astronomy/generate/generate.c
2025-01-27 11:50:43 -05:00

2210 lines
68 KiB
C

/*
MIT License
Copyright (c) 2019-2025 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"
#include "top2013.h"
const double PLUTO_TOLERANCE_ARCMIN = 1.4;
int Verbose;
#define DEBUG(...) do{if(Verbose)printf(__VA_ARGS__);}while(0)
#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 ParseDate(const char *text, double *tt);
static int OpenEphem(void);
static int PrintUsage(void);
static int GenerateVsopPlanets(void);
static int TopCalc(const char *name, const char *date);
static int GenerateApsisTestData(void);
static int OptimizeJupiterMoons(const char *inFileName, const char *outFileName);
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 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);
static int DistancePlot(const char *name, double tt1, double tt2);
static int ImproveVsopApsides(vsop_model_t *model);
static int DeltaTimePlot(const char *outFileName);
static int TopFileInfo(const char *filename, const char *name);
static int ValidateTop2013(void);
static int Diff(const char *filename1, const char *filename2, int *nlines);
static int GenerateGalEqjTestData(const char *outFileName);
#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 > 1 && !strcmp(argv[1], "-v"))
{
Verbose = 1;
--argc;
++argv;
}
if (argc == 2 && !strcmp(argv[1], "planets"))
return GenerateVsopPlanets();
if (argc == 2 && !strcmp(argv[1], "apsis"))
return GenerateApsisTestData();
if (argc == 2 && !strcmp(argv[1], "jmopt"))
return OptimizeJupiterMoons("jupiter_moons/fortran/BisL1.2.dat", "output/jupiter_moons.txt");
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();
if (argc == 5 && !strcmp(argv[1], "distplot"))
return DistancePlot(argv[2], atof(argv[3]), atof(argv[4]));
if (argc == 3 && !strcmp(argv[1], "dtplot"))
return DeltaTimePlot(argv[2]);
if (argc == 4 && !strcmp(argv[1], "topcalc"))
return TopCalc(argv[2], argv[3]);
if (argc == 2 && !strcmp(argv[1], "validate_top2013"))
return ValidateTop2013();
if (argc == 4 && !strcmp(argv[1], "topinfo"))
return TopFileInfo(argv[2], argv[3]);
if (argc == 3 && !strcmp(argv[1], "galeqj"))
return GenerateGalEqjTestData(argv[2]);
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"
"generate distplot planet tt1 tt2\n"
" Generate a CSV plot of the given planet's heliocentric\n"
" distance as a function of time.\n"
"\n"
"generate dtplot outfile.csv\n"
" Generate a CSV plot of the Delta-T extrapolator.\n"
"\n"
"generate validate_top2013\n"
" Validates code for calculating outer planet positions using TOP2013.\n"
"\n"
"generate topcalc planet date\n"
" Calculate an exact position and velocity of the given planet\n"
" (Jupiter, Saturn, Uranus, Neptune, or Pluto)\n"
" at the specified date and time.\n"
"\n"
"generate topinfo filename planet\n"
" Prints summary info about the TOP2013 file.\n"
"\n"
"generate galeqj outfile.txt\n"
" Generate test data to validate conversion between\n"
" galatic coordinates (GAL) and equatorial J2000 (EQJ).\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;
}
#define NovasBodyPos(jd,body,pos) NovasBodyPosVel(jd,body,pos,NULL)
/*
* Returns heliocentric position and velocity vectors based on NOVAS
* interpolation of the ephemeris file. The jd must be a Julian Date
* in the range of years 1600..2200.
*/
static int NovasBodyPosVel(double jd, int body, double pos[3], double vel[3])
{
int error, k;
double jed[2];
double sun_pos[3], moon_pos[3];
double sun_vel[3], moon_vel[3];
double ignore_vel[3];
if (vel == NULL)
vel = ignore_vel;
jed[0] = jd;
jed[1] = 0.0;
if (body == BODY_SSB)
{
/*
The NOVAS state() function does everything with respect to
the Solar System Barycenter (SSB) already.
Below we will get the Sun's position relative to the SSB.
Negating that will result in the SSB position with respect to the Sun.
*/
pos[0] = pos[1] = pos[2] = 0.0;
vel[0] = vel[1] = vel[2] = 0.0;
}
else 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)
FAIL("NovasBodyPos: state(%lf, EMB) returned %d\n", jd, error);
error = state(jed, BODY_GM, moon_pos, moon_vel);
if (error)
FAIL("NovasBodyPos: state(%lf, GM) returned %d\n", jd, 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];
vel[k] += factor * moon_vel[k];
}
}
else
{
/* This is a body that NOVAS directly models in its ephemerides. */
error = (int)state(jed, (short)body, pos, vel);
if (error)
FAIL("NovasBodyPos: state(%lf, %d) returned %d\n", jd, body, 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, sun_vel);
if (error)
FAIL("NovasBodyPos: state(%lf, SUN) returned %d\n", jd, error);
pos[0] -= sun_pos[0];
pos[1] -= sun_pos[1];
pos[2] -= sun_pos[2];
vel[0] -= sun_vel[0];
vel[1] -= sun_vel[1];
vel[2] -= sun_vel[2];
error = 0;
fail:
return error;
}
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 = ImproveVsopApsides(&model);
if (error) goto fail;
VsopTrim(&model); /* shave off all trailing empty series right before saving */
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 - T0, jdStop - T0, threshold);
if (error) goto fail;
*trunc_terms = VsopTermCount(model);
for (jd = jdStart; jd <= jdStop; jd += jdDelta)
{
VsopCalcPos(model, jd - T0, 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 TermContribution(const vsop_term_t *term, int sindex, double *contrib)
{
switch (sindex)
{
case 0:
/* t^0 term has derivative proprotional to amplitude*frequency */
*contrib = fabs(term->amplitude * term->frequency);
return 0;
default:
fprintf(stderr, "TermContribution: t^%d derivative rule not yet implemented.\n", sindex);
*contrib = NAN;
return 1;
}
}
static int ExpandSeries(vsop_formula_t *formula, int sindex, int n_top_terms)
{
int error;
vsop_series_t *series;
int t, t_best, n;
double contrib, max_contrib;
/*
Examine the specified truncated series in the given formula.
Calculate an estimate of the contribution of every term's
first derivative. Pick n_top_terms of the most important ones.
*/
if (sindex < 0 || sindex >= formula->nseries_calc)
{
fprintf(stderr, "ExpandSeries: Invalid series index %d\n", sindex);
return 1;
}
series = &formula->series[sindex];
if (series->nterms_calc + n_top_terms >= series->nterms_total)
{
fprintf(stderr, "ExpandSeries: series calc=%d, total=%d; cannot expand by %d\n",
series->nterms_calc, series->nterms_total, n_top_terms);
return 1;
}
for (n=0; n < n_top_terms; ++n)
{
/* Iterate through the truncated terms at the end of this series. */
/* Pick the one whose contribution is greatest and append it next. */
t_best = -1;
max_contrib = 0.0;
for (t = series->nterms_calc; t < series->nterms_total; ++t)
{
CHECK(TermContribution(&series->term[t], sindex, &contrib));
if (contrib > max_contrib)
{
t_best = t;
max_contrib = contrib;
}
}
if (t_best < 0)
{
fprintf(stderr, "ExpandSeries: Found only %d of %d top terms\n", n, n_top_terms);
return 1;
}
/* Like bubble sort: swap best with whatever is already just beyond the truncation. */
if (t_best != series->nterms_calc)
{
vsop_term_t swap = series->term[series->nterms_calc];
series->term[series->nterms_calc] = series->term[t_best];
series->term[t_best] = swap;
}
++(series->nterms_calc);
}
error = 0;
fail:
return error;
}
static int ImproveVsopApsides(vsop_model_t *model)
{
int error;
int n_top_terms;
vsop_formula_t *radform; /* formula for calculating radial distance */
if (model->version != VSOP_HELIO_SPHER_DATE && model->version != VSOP_HELIO_SPHER_J2000)
FAIL("ImproveVsopApsides: Cannot optimize VSOP version %d\n", model->version);
if (model->ncoords != 3)
FAIL("ImproveVsopApsides: INTERNAL ERROR: model has incorrect number of coordinates: %d\n", model->ncoords);
radform = &model->formula[2];
switch (model->body)
{
case BODY_VENUS:
n_top_terms = 5;
break;
case BODY_EARTH:
n_top_terms = 5;
break;
case BODY_JUPITER:
n_top_terms = 3;
break;
case BODY_SATURN:
n_top_terms = 7;
break;
case BODY_URANUS:
n_top_terms = 3;
break;
case BODY_NEPTUNE:
n_top_terms = 5;
break;
default:
n_top_terms = 0;
break;
}
CHECK(ExpandSeries(radform, 0, n_top_terms));
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 GenerateVsopPlanets(void)
{
int error;
CHECK(OpenEphem());
CHECK(BuildVsopData());
fail:
ephem_close();
return error;
}
static double PlanetOrbitalPeriod(int body)
{
switch (body)
{
case BODY_MERCURY: return 87.969;
case BODY_VENUS: return 224.701;
case BODY_EARTH: return 365.256;
case BODY_MARS: return 686.980;
case BODY_JUPITER: return 4332.589;
case BODY_SATURN: return 10759.22;
case BODY_URANUS: return 30685.4;
case BODY_NEPTUNE: return 60189.0;
case BODY_PLUTO: return 90560.0;
default: return 0.0; /* invalid body */
}
}
static int SearchApsis(int body, double direction, double jd1, double jd2, double *jdx, double *xdist)
{
int error;
const int npoints = 10;
int i, best_i;
double jd;
double pos[3];
double interval, dist, max_dist;
*xdist = *jdx = NAN;
/* Keep iterating until interval is within 1 second of uncertainty. */
while (jd2-jd1 > 1.0 / 86400.0)
{
interval = (jd2 - jd1) / (npoints - 1.0);
max_dist = NAN;
best_i = -1;
for (i=0; i < npoints; ++i)
{
jd = jd1 + (i*interval);
CHECK(NovasBodyPos(jd, body, pos));
dist = direction * VectorLength(pos);
if (i==0 || dist > max_dist)
{
best_i = i;
max_dist = dist;
}
}
/* Narrow in on +/- 1 interval around the extreme point. */
jd1 += (best_i - 1)*interval;
jd2 = jd1 + (2 * interval);
}
*jdx = (jd1 + jd2) / 2.0;
CHECK(NovasBodyPos(*jdx, body, pos));
*xdist = VectorLength(pos);
error = 0;
fail:
return error;
}
static void PrintApsis(FILE *outfile, int kind, double jdx, double dist)
{
short year, month, day;
int hour, minute, second;
double frac;
cal_date(jdx, &year, &month, &day, &frac);
hour = (int)floor(frac);
frac = 60.0 * (frac - hour);
minute = (int)floor(frac);
frac = 60.0 * (frac - minute);
second = (int)floor(frac);
fprintf(outfile, "%d %04d-%02d-%02dT%02d:%02d:%02dZ %12.7lf\n", kind, (int)year, (int)month, (int)day, hour, minute, second, dist);
}
static int GenerateApsisFile(int body, const char *outFileName)
{
int error = 1;
FILE *outfile = NULL;
double pos[3];
double dist[3]; /* dist[0]=current distance, dist[1]=previous distance, dist[2]=distance before that */
double jdStart, jdStop, jd;
double period, interval;
const double samples_per_orbit = 1000.0;
struct { double dist; double jd; int kind; } buffer[3];
int buflen = 0;
period = PlanetOrbitalPeriod(body);
if (period <= 0.0)
{
fprintf(stderr, "GenerateApsisFile: Invalid body %d\n", body);
error = 1;
goto fail;
}
interval = period / samples_per_orbit;
outfile = fopen(outFileName, "wt");
if (outfile == NULL)
{
fprintf(stderr, "GenerateApsisFile: Cannot open output file '%s'\n", outFileName);
error = 1;
goto fail;
}
jdStart = julian_date(MIN_YEAR, 1, 1, 0.0);
jdStop = julian_date(MAX_YEAR, 1, 1, 0.0);
dist[0] = dist[1] = dist[2] = -1.0;
for (jd=jdStart; jd <= jdStop; jd += interval)
{
CHECK(NovasBodyPos(jd, body, pos));
dist[0] = VectorLength(pos);
if (dist[2] > 0.0)
{
if (dist[1] >= dist[2] && dist[1] >= dist[0])
{
buffer[buflen].kind = 1; /* aphelion */
CHECK(SearchApsis(body, +1.0, jd-2.0*interval, jd, &buffer[buflen].jd, &buffer[buflen].dist));
++buflen;
}
else if (dist[1] <= dist[2] && dist[1] <= dist[0])
{
buffer[buflen].kind = 0; /* perihelion */
CHECK(SearchApsis(body, -1.0, jd-2.0*interval, jd, &buffer[buflen].jd, &buffer[buflen].dist));
++buflen;
}
/*
We have to handle weird special cases with Neptune.
It can have 3 local minima/maxima near perihelion and aphelion,
due to Sun wobbling around Solar System Barycenter.
*/
if (buflen > 0)
{
if (body == BODY_NEPTUNE)
{
const double mean_dist = 30.1; /* mean orbital distance of Neptune in AU */
if (buflen == 3)
{
/* If all 3 entries are on the same side of the mean distance, pick the extreme. */
if (buffer[0].dist > mean_dist && buffer[1].dist > mean_dist && buffer[2].dist > mean_dist)
{
int imax = 0;
if (buffer[1].dist > buffer[imax].dist) imax = 1;
if (buffer[2].dist > buffer[imax].dist) imax = 2;
if (buffer[imax].kind != 1)
{
fprintf(stderr, "FATAL(GenerateApsisFile): expected Neptune aphelion\n");
error = 1;
goto fail;
}
PrintApsis(outfile, buffer[imax].kind, buffer[imax].jd, buffer[imax].dist);
buflen = 0; /* empty the buffer */
}
else if (buffer[0].dist < mean_dist && buffer[1].dist < mean_dist && buffer[2].dist < mean_dist)
{
int imin = 0;
if (buffer[1].dist < buffer[imin].dist) imin = 1;
if (buffer[2].dist < buffer[imin].dist) imin = 2;
if (buffer[imin].kind != 0)
{
fprintf(stderr, "FATAL(GenerateApsisFile): expected Neptune perihelion\n");
error = 1;
goto fail;
}
PrintApsis(outfile, buffer[imin].kind, buffer[imin].jd, buffer[imin].dist);
buflen = 0; /* empty the buffer */
}
else
{
/* We have to fail because the buffer is full and we don't know how to empty it. */
fprintf(stderr, "FATAL(GenerateApsisFile): Unhandled special case with Neptune apsis\n");
error = 1;
goto fail;
}
}
}
else
{
PrintApsis(outfile, buffer[0].kind, buffer[0].jd, buffer[0].dist);
buflen = 0;
}
}
}
dist[2] = dist[1];
dist[1] = dist[0];
}
if (buflen > 0)
{
fprintf(stderr, "FATAL(GenerateApsisFile): buffer holds %d unhandled events.\n", buflen);
error = 1;
goto fail;
}
fail:
if (outfile)
{
fclose(outfile);
if (error) remove(outFileName);
}
return error;
}
static int GenerateApsisTestData(void)
{
int error;
CHECK(OpenEphem());
/*
Tricky: BODY_EARTH=11, but we write to output/apsis_2.txt.
The BODY_EARTH is my extension here for working around NOVAS,
but Astronomy Engine uses 2 to represent the Earth.
*/
CHECK(GenerateApsisFile(BODY_MERCURY, "apsides/apsis_0.txt"));
CHECK(GenerateApsisFile(BODY_VENUS, "apsides/apsis_1.txt"));
CHECK(GenerateApsisFile(BODY_EARTH, "apsides/apsis_2.txt"));
CHECK(GenerateApsisFile(BODY_MARS, "apsides/apsis_3.txt"));
CHECK(GenerateApsisFile(BODY_JUPITER, "apsides/apsis_4.txt"));
CHECK(GenerateApsisFile(BODY_SATURN, "apsides/apsis_5.txt"));
CHECK(GenerateApsisFile(BODY_URANUS, "apsides/apsis_6.txt"));
CHECK(GenerateApsisFile(BODY_NEPTUNE, "apsides/apsis_7.txt"));
CHECK(GenerateApsisFile(BODY_PLUTO, "apsides/apsis_8.txt"));
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.ts", "template/astronomy.ts", "output"));
CHECK(GenerateCode(CODEGEN_LANGUAGE_PYTHON, "../source/python/astronomy/astronomy.py", "template/astronomy.py", "output"));
CHECK(GenerateCode(CODEGEN_LANGUAGE_KOTLIN, "../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt", "template/astronomy.kt", "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;
}
if ((body != BODY_SSB) && (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 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;
double tolerance;
*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);
tolerance = (body == BODY_PLUTO) ? 1.3 : 1.0;
*arcmin = fabs(diff * 60.0);
if (*arcmin > tolerance)
{
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];
double tolerance;
*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;
}
tolerance = (body == BODY_PLUTO) ? PLUTO_TOLERANCE_ARCMIN : 0.4;
if (*arcmin > tolerance)
{
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;
double delta_t_seconds;
double tolerance;
*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;
}
delta_t_seconds = (24.0 * 3600.0) * (tt - ut);
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, delta_t_seconds, 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);
tolerance = (body == BODY_PLUTO) ? PLUTO_TOLERANCE_ARCMIN : 0.9;
if (*arcmin_equ > tolerance)
{
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, delta_t_seconds, 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);
tolerance = (body == BODY_PLUTO) ? PLUTO_TOLERANCE_ARCMIN : 0.9;
if (*arcmin_hor > tolerance)
{
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;
if (!strcmp(name, "SSB")) return BODY_SSB;
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 VSOP_PLUTO: name = "Pluto"; break;
case VSOP_GM: name = "GM"; break;
case VSOP_SUN: name = "Sun"; break;
case VSOP_MOON: name = "Moon"; break;
case VSOP_SSB: name = "SSB"; 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;
vsop_body_t body;
FILE *infile = NULL;
char line[300];
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 '#': /* ignore debug output */
case 'j': /* ignore Jupiter moons calculations: for diff testing only */
case 'n': /* ignore nutation calculations: for diff testing only */
case 'm': /* ignore EclipticGeoMoon */
break;
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);
if (Verbose)
{
int nprinted, 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);
}
printf("\n");
}
error = 0;
fail:
ephem_close();
if (infile != NULL) fclose(infile);
return error;
}
static int TestChebFunc(const void *context, double t, double f[])
{
(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;
}
static int DistancePlot(const char *name, double tt1, double tt2)
{
int error;
vsop_body_t body;
double pos[3];
double tt, dist;
int i;
const int npoints = 100000;
CHECK(OpenEphem());
body = LookupBody(name);
if (body == BODY_INVALID)
FAIL("DistancePlot: Invalid body name '%s'\n", name);
printf("\"tt\",\"distance\",\"x\",\"y\",\"z\"\n");
for (i=0; i < npoints; ++i)
{
tt = tt1 + (((double)i)/((double)(npoints-1)) * (tt2 - tt1));
CHECK(NovasBodyPos(tt + T0, body, pos));
dist = VectorLength(pos);
printf("%0.16lf,%0.16lg,%0.16lg,%0.16lg,%0.16lg\n", tt, dist, pos[0], pos[1], pos[2]);
}
error = 0;
fail:
ephem_close();
return error;
}
static int DeltaTimePlot(const char *outFileName)
{
int error = 1;
FILE *outfile = NULL;
const int minYear = 1500;
const int maxYear = 2500;
int year;
double dt;
outfile = fopen(outFileName, "wt");
if (outfile == NULL)
FAIL("DeltaTimePlot: Cannot open output file: %s\n", outFileName);
fprintf(outfile, "\"year\",\"delta_t\"\n");
for (year=minYear; year <= maxYear; ++year)
{
dt = ExtrapolatedDeltaT(year);
fprintf(outfile, "%d,%lf\n", year, dt);
}
printf("DeltaTimePlot: Wrote file %s\n", outFileName);
error = 0;
fail:
if (outfile != NULL) fclose(outfile);
return error;
}
/*-----------------------------------------------------------------------------------------*/
static const char *TopDataFileName = "TOP2013.dat";
static int CalcTop2013(FILE *outfile, const top_model_t *model)
{
int error = 1;
double jd, tt;
int i;
top_elliptical_t ellip;
top_rectangular_t ecl, equ;
double pos[3];
double range, diff, dx, dy, dz;
double max_diff = 0.0;
for (i=0; i <= 10; ++i)
{
tt = 4000.0 * (i-10);
jd = tt + 2451545.0;
fprintf(outfile, "%d %0.1lf\n", model->planet, jd);
CHECK(TopCalcElliptical(model, tt, &ellip));
fprintf(outfile, " %15.10lf %15.10lf %15.10lf %15.10lf %15.10lf %15.10lf\n", ellip.a, ellip.lambda, ellip.k, ellip.h, ellip.q, ellip.p);
CHECK(TopEcliptic(model->planet, &ellip, &ecl));
fprintf(outfile, " %15.10lf %15.10lf %15.10lf %15.10lf %15.10lf %15.10lf\n", ecl.x, ecl.y, ecl.z, ecl.vx, ecl.vy, ecl.vz);
CHECK(TopEquatorial(&ecl, &equ));
fprintf(outfile, " %15.10lf %15.10lf %15.10lf %15.10lf %15.10lf %15.10lf\n", equ.x, equ.y, equ.z, equ.vx, equ.vy, equ.vz);
/* Compare the equatorial vector against NOVAS calculations. */
CHECK(NovasBodyPos(jd, model->planet - 1, pos));
range = sqrt(pos[0]*pos[0] + pos[1]*pos[1] + pos[2]*pos[2]);
dx = pos[0] - equ.x;
dy = pos[1] - equ.y;
dz = pos[2] - equ.z;
diff = 60.0 * RAD2DEG * (sqrt(dx*dx + dy*dy + dz*dz) / range);
if (diff > max_diff)
max_diff = diff;
DEBUG("planet %d jd %10.1lf arcmin %10.6lf\n", model->planet, jd, diff);
}
printf("CalcTop2013: planet=%d, max arcmin error=%lg\n", model->planet, max_diff);
error = 0;
fail:
return error;
}
static int ValidateTop2013(void)
{
int error = 1;
top_model_t model;
FILE *mirror = NULL;
FILE *outfile = NULL;
int nlines, planet;
const char *mirrorFileName = "TOP2013.mirror";
const char *outFileName = "top2013/calc.txt";
TopInitModel(&model);
CHECK(OpenEphem());
outfile = fopen(outFileName, "wt");
if (outfile == NULL)
FAIL("ValidateTop2013: Cannot open output file: %s\n", outFileName);
mirror = fopen(mirrorFileName, "wt");
if (mirror == NULL)
FAIL("ValidateTop2013: cannot open output file: %s\n", mirrorFileName);
for (planet=5; planet <= 9; ++planet)
{
CHECK(TopLoadModel(&model, TopDataFileName, planet));
CHECK(TopWriteModel(&model, mirror));
CHECK(CalcTop2013(outfile, &model));
TopFreeModel(&model);
}
fclose(mirror);
mirror = NULL;
fclose(outfile);
outfile = NULL;
/* Verify that the saved model exactly matches what we loaded. */
CHECK(Diff(TopDataFileName, mirrorFileName, &nlines));
if (nlines != 336806)
FAIL("ValidateTop2013(%s): incorrect number of matching lines = %d\n", mirrorFileName, nlines);
/* Verify that the calculations exactly match those produced by the original FORTRAN code. */
CHECK(Diff(outFileName, "top2013/correct.txt", NULL));
/* Clean up after success. */
remove(mirrorFileName);
remove(outFileName);
printf("ValidateTop2013: PASS\n");
error = 0;
fail:
ephem_close();
if (mirror) fclose(mirror);
if (outfile) fclose(outfile);
TopFreeModel(&model);
return error;
}
static int Diff(const char *filename1, const char *filename2, int *nlines)
{
int lnum = 0;
int error = 1;
FILE *infile1 = NULL;
FILE *infile2 = NULL;
char line1[200];
char line2[200];
infile1 = fopen(filename1, "rt");
if (infile1 == NULL)
FAIL("Diff: cannot open input file: %s\n", filename1);
infile2 = fopen(filename2, "rt");
if (infile2 == NULL)
FAIL("Diff: cannot open input file: %s\n", filename2);
for(;;)
{
const char *r1 = fgets(line1, sizeof(line1), infile1);
const char *r2 = fgets(line2, sizeof(line2), infile2);
if (r1 == NULL && r2 == NULL)
break;
if (r1 == NULL)
FAIL("Diff: %s is shorter than %s\n", filename1, filename2);
if (r2 == NULL)
FAIL("Diff: %s is shorter than %s\n", filename2, filename1);
++lnum;
if (strcmp(line1, line2))
{
fprintf(stderr, "[%s]\n", line1);
fprintf(stderr, "[%s]\n", line2);
FAIL("Diff(%d): lines are different.\n", lnum);
}
}
printf("Diff: %d identical lines in files %s and %s\n", lnum, filename1, filename2);
error = 0;
fail:
if (infile1 != NULL) fclose(infile1);
if (infile2 != NULL) fclose(infile2);
if (nlines != NULL) *nlines = lnum;
return error;
}
static int TopCalc(const char *name, const char *date)
{
int error = 1;
top_model_t model;
vsop_body_t body;
top_rectangular_t equ;
int planet;
double tt;
TopInitModel(&model);
CHECK(ParseDate(date, &tt));
body = LookupBody(name);
if (body < 0)
FAIL("TopCalc: planet name '%s' is not valid.\n", name);
if (body < BODY_JUPITER || body > BODY_PLUTO)
FAIL("TopCalc: TOP2013 supports Jupiter through Pluto only.\n");
planet = body + 1; /* convert our body ID into TOP2013 planet ID */
CHECK(TopLoadModel(&model, TopDataFileName, planet));
CHECK(TopPosition(&model, tt, &equ));
printf("tt = %0.8lf\n", tt);
printf("pos = [%22.16lf, %22.16lf, %22.16lf]\n", equ.x, equ.y, equ.z);
printf("vel = [%22.16lf, %22.16lf, %22.16lf]\n", equ.vx, equ.vy, equ.vz);
error = 0;
fail:
TopFreeModel(&model);
return error;
}
static int TopFileInfo(const char *filename, const char *name)
{
int error = 1;
top_model_t model;
vsop_body_t body;
int planet, f;
TopInitModel(&model);
body = LookupBody(name);
if (body < 0)
FAIL("TopFileInfo: planet name '%s' is not valid.\n", name);
planet = body + 1; /* convert our body ID into TOP2013 planet ID */
CHECK(TopLoadModel(&model, filename, planet));
printf("%6d [", TopTermCount(&model));
for (f=0; f < TOP_NCOORDS; ++f)
printf("%6d", TopTermCountF(&model, f));
printf("]\n");
error = 0;
fail:
TopFreeModel(&model);
return error;
}
/*------------------------------------------------------------------------------------------------*/
static int ParseDate(const char *text, double *tt)
{
int error = 1;
int nscanned;
int year, month, day, hour, minute;
double second, float_hours;
/* Allow passing in an explicit tt value directly. */
if (text[0] == '@' && 1 == sscanf(text+1, "%lf", tt))
return 0;
nscanned = sscanf(text, "%d-%d-%dT%d:%d:%lfZ", &year, &month, &day, &hour, &minute, &second);
if (nscanned < 5)
FAIL("ParseDate: text '%s' is not valid. Must be formatted as yyyy-mm-dd[Thh:mm[:ss.sss]Z].\n", text);
if (nscanned < 6)
second = 0.0;
float_hours = hour + minute/60.0 + second/3600.0;
*tt = julian_date((short)year, (short)month, (short)day, float_hours) - T0;
error = 0;
fail:
return error;
}
/*------------------------------------------------------------------------------------------------*/
static void JupiterMoon_elem2pv(double mu, const double elem[6], double state[6])
{
/* Translation of FORTRAN subroutine ELEM2PV from: */
/* https://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/ */
double EE, DE, CE, SE, DLE, RSAM1, ASR, PHI, PSI, X1, Y1, VX1, VY1, F2, P2, Q2, PQ;
const double A = elem[0];
const double AL = elem[1];
const double K = elem[2];
const double H = elem[3];
const double Q = elem[4];
const double P = elem[5];
const double AN = sqrt(mu / (A*A*A));
EE = AL + K*sin(AL) - H*cos(AL);
do
{
CE = cos(EE);
SE = sin(EE);
DE = (AL - EE + K*SE - H*CE) / (1.0 - K*CE - H*SE);
EE += DE;
}
while (fabs(DE) >= 1.0e-12);
CE = cos(EE);
SE = sin(EE);
DLE = H*CE - K*SE;
RSAM1 = -K*CE - H*SE;
ASR = 1.0/(1.0 + RSAM1);
PHI = sqrt(1.0 - K*K - H*H);
PSI = 1.0/(1.0 + PHI);
X1 = A*(CE - K - PSI*H*DLE);
Y1 = A*(SE - H + PSI*K*DLE);
VX1 = AN*ASR*A*(-SE - PSI*H*RSAM1);
VY1 = AN*ASR*A*(+CE + PSI*K*RSAM1);
F2 = 2.0*sqrt(1.0 - Q*Q - P*P);
P2 = 1.0 - 2.0*P*P;
Q2 = 1.0 - 2.0*Q*Q;
PQ = 2.0*P*Q;
/* position vector */
state[0] = X1*P2 + Y1*PQ;
state[1] = X1*PQ + Y1*Q2;
state[2] = (Q*Y1 - X1*P)*F2;
/* velocity vector */
state[3] = VX1*P2 + VY1*PQ;
state[4] = VX1*PQ + VY1*Q2;
state[5] = (Q*VY1 - VX1*P)*F2;
}
static void CalcJupiterMoon(const jupiter_moon_model_t *model, int mindex, int trunc_flag, double tt, double state[6])
{
/* This is a translation of FORTRAN code by Duriez, Lainey, and Vienne: */
/* https://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/ */
const double PI2 = (2.0 * 3.14159265358979323846);
int k, nterms;
double arg;
double elem[6];
const jupiter_moon_t *m = &model->moon[mindex];
const double t = tt + 18262.5; /* t = time since 1950-01-01T00:00:00Z */
/* Calculate 6 orbital elements at the given time t. */
elem[0] = 0.0;
nterms = trunc_flag ? m->a.nterms_calc : m->a.nterms_total;
for (k = 0; k < nterms; ++k)
{
arg = m->a.term[k].phase + (t * m->a.term[k].frequency);
elem[0] += m->a.term[k].amplitude * cos(arg);
}
elem[1] = m->al[0] + (t * m->al[1]);
nterms = trunc_flag ? m->l.nterms_calc : m->l.nterms_total;
for (k = 0; k < nterms; ++k)
{
arg = m->l.term[k].phase + (t * m->l.term[k].frequency);
elem[1] += m->l.term[k].amplitude * sin(arg);
}
elem[1] = fmod(elem[1], PI2);
if (elem[1] < 0.0)
elem[1] += PI2;
elem[2] = elem[3] = 0.0;
nterms = trunc_flag ? m->z.nterms_calc : m->z.nterms_total;
for (k = 0; k < nterms; ++k)
{
arg = m->z.term[k].phase + (t * m->z.term[k].frequency);
elem[2] += m->z.term[k].amplitude * cos(arg);
elem[3] += m->z.term[k].amplitude * sin(arg);
}
elem[4] = elem[5] = 0.0;
nterms = trunc_flag ? m->zeta.nterms_calc : m->zeta.nterms_total;
for (k = 0; k < nterms; ++k)
{
arg = m->zeta.term[k].phase + (t * m->zeta.term[k].frequency);
elem[4] += m->zeta.term[k].amplitude * cos(arg);
elem[5] += m->zeta.term[k].amplitude * sin(arg);
}
/* Convert the oribital elements into position vectors in the Jupiter equatorial system (JUP). */
JupiterMoon_elem2pv(m->mu, elem, state);
}
static double JupiterMoonRelativeError(const jupiter_moon_model_t *model, int mindex)
{
const double tt1 = -2000.0;
const double tt2 = +2000.0;
const double dt = 0.1;
double tt;
int n = 0;
double sum = 0.0;
double state_full[6];
double state_trunc[6];
double dx, dy, dz;
double rx, ry, rz;
for (tt = tt1; tt <= tt2; tt += dt)
{
/* Calculate without truncation. */
CalcJupiterMoon(model, mindex, 0, tt, state_full);
/* Calculate with truncation. */
CalcJupiterMoon(model, mindex, 1, tt, state_trunc);
/* Tally the dimensionless relative position error introduced by the truncation. */
rx = state_full[0];
ry = state_full[1];
rz = state_full[2];
dx = rx - state_trunc[0];
dy = ry - state_trunc[1];
dz = rz - state_trunc[2];
sum += (dx*dx + dy*dy + dz*dz) / (rx*rx + ry*ry + rz*rz);
/* Tally the dimensionless relative velocity error introduced by the truncation. */
rx = state_full[3];
ry = state_full[4];
rz = state_full[5];
dx = rx - state_trunc[3];
dy = ry - state_trunc[4];
dz = rz - state_trunc[5];
sum += (dx*dx + dy*dy + dz*dz) / (rx*rx + ry*ry + rz*rz);
/* Account for 2 additional error terms in the sum. */
n += 2;
}
return sqrt(sum) / n;
}
static int JupiterMoonTruncate(jupiter_moon_model_t *model, int mindex)
{
const double threshold = 3.5e-7;
int error;
jupiter_moon_t *moon = &model->moon[mindex];
double score, best_score;
int winner; /* Which truncation is best? 0=a, 1=l, 2=z, 3=zeta. */
for(;;)
{
/* Chop off one term at a time from each variable's series. */
/* Pick whichever incremental truncation causes the least damage to the accuracy score. */
/* Keep going until we can't chop of any more terms without exceeding relative error threshold. */
winner = -1;
best_score = 1.0e+99;
if (moon->a.nterms_calc > 1)
{
--moon->a.nterms_calc;
score = JupiterMoonRelativeError(model, mindex);
++moon->a.nterms_calc;
if (score < best_score)
{
best_score = score;
winner = 0;
}
}
if (moon->l.nterms_calc > 1)
{
--moon->l.nterms_calc;
score = JupiterMoonRelativeError(model, mindex);
++moon->l.nterms_calc;
if (score < best_score)
{
best_score = score;
winner = 1;
}
}
if (moon->z.nterms_calc > 1)
{
--moon->z.nterms_calc;
score = JupiterMoonRelativeError(model, mindex);
++moon->z.nterms_calc;
if (score < best_score)
{
best_score = score;
winner = 2;
}
}
if (moon->zeta.nterms_calc > 1)
{
--moon->zeta.nterms_calc;
score = JupiterMoonRelativeError(model, mindex);
++moon->zeta.nterms_calc;
if (score < best_score)
{
best_score = score;
winner = 3;
}
}
if (winner < 0)
{
printf("JupiterMoonTruncate: mindex=%d : no more truncation is available!\n", mindex);
break;
}
DEBUG("JupiterMoonTruncate: mindex=%d, winner=%d, best_score=%le, nterms=[a:%d/%d, l:%d/%d, z:%d/%d, zeta:%d/%d]\n",
mindex, winner, best_score,
moon->a.nterms_calc, moon->a.nterms_total,
moon->l.nterms_calc, moon->l.nterms_total,
moon->z.nterms_calc, moon->z.nterms_total,
moon->zeta.nterms_calc, moon->zeta.nterms_total);
if (best_score > threshold)
{
printf("JupiterMoonTruncate: reached threshold, mindex=%d, winner=%d, best_score=%le, nterms=[a:%d/%d, l:%d/%d, z:%d/%d, zeta:%d/%d]\n",
mindex, winner, best_score,
moon->a.nterms_calc, moon->a.nterms_total,
moon->l.nterms_calc, moon->l.nterms_total,
moon->z.nterms_calc, moon->z.nterms_total,
moon->zeta.nterms_calc, moon->zeta.nterms_total);
break;
}
switch (winner)
{
case 0: --moon->a.nterms_calc; break;
case 1: --moon->l.nterms_calc; break;
case 2: --moon->z.nterms_calc; break;
case 3: --moon->zeta.nterms_calc; break;
default: FAIL("JupiterMoonTruncate: Internal error: winner=%d\n", winner);
}
}
error = 0;
fail:
return error;
}
static int OptimizeJupiterMoons(const char *inFileName, const char *outFileName)
{
int error, mindex;
jupiter_moon_model_t *model;
model = calloc(1, sizeof(jupiter_moon_model_t));
if (model == NULL)
FAIL("OptimizeJupiterMoons: memory allocation failure.\n");
CHECK(LoadJupiterMoonModel(inFileName, model));
for (mindex = 0; mindex < NUM_JUPITER_MOONS; ++mindex)
CHECK(JupiterMoonTruncate(model, mindex));
CHECK(SaveJupiterMoonModel(outFileName, model));
error = 0;
fail:
free(model);
return error;
}
/*------------------------------------------------------------------------------------------------*/
static int GenerateGalEqjTestData(const char *outFileName)
{
/*
[2021-06-06 / Don Cross] Based on the discussion at:
https://github.com/cosinekitty/astronomy/discussions/112
I am adding support for converting between galactic (GAL)
and equatorial J2000 (EQJ) orientations.
I will start with test data generated by NOVAS C 3.1.
NOVAS provides a function equ2gal() that converts ICRS
(the same as EQJ within Astronomy Engine's error tolerances)
to galactic coordinates.
This function generates some test data that contains rows of
data formatted as
RA DEC glon glat
where
RA = EQJ right ascension in sidereal hours
DEC = EQJ declination in degrees
glon = GAL longitude in degrees
glat = GAL latitude in degrees
*/
int error;
FILE *outfile;
double ra, dec, glon, glat;
int i, k;
const int NSTEPS = 12;
outfile = fopen(outFileName, "wt");
if (outfile == NULL)
FAIL("GenerateGalEqjTestData: Cannot open output file: %s\n", outFileName);
for (i = 0; i < NSTEPS; ++i)
{
ra = (24.0 * i) / NSTEPS;
for (k = 1; k < NSTEPS; ++k)
{
dec = -90.0 + ((180.0 * k) / NSTEPS);
equ2gal(ra, dec, &glon, &glat);
fprintf(outfile, "%6.1lf %6.1lf %20.16lf %20.16lf\n", ra, dec, glon, glat);
}
}
printf("GenerateGalEqjTestData: Wrote output file: %s\n", outFileName);
error = 0;
fail:
if (outfile != NULL) fclose(outfile);
return error;
}
/*------------------------------------------------------------------------------------------------*/