mirror of
https://github.com/cosinekitty/astronomy.git
synced 2026-04-03 22:34:53 -04:00
Also made time parameters to rotation matrix functions be of type FlexibleDateTime, and internally convert them to AstroTime. This should be the policy of all exposed functions in the JavaScript version of Astronomy Engine. Exposed KM_PER_AU to outside callers.
1930 lines
76 KiB
JavaScript
1930 lines
76 KiB
JavaScript
'use strict';
|
|
const fs = require('fs');
|
|
const Astronomy = require('../source/js/astronomy.min.js');
|
|
let Verbose = false;
|
|
const DEG2RAD = 0.017453292519943296;
|
|
const RAD2DEG = 57.295779513082321;
|
|
|
|
function Fail(message) {
|
|
console.trace();
|
|
console.log(`FATAL(test.js): ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function v(x) {
|
|
// Verify that 'x' is really a number.
|
|
|
|
if (!Number.isFinite(x)) {
|
|
console.trace();
|
|
throw `Not a finite number: ${x}`;
|
|
}
|
|
|
|
return x;
|
|
}
|
|
|
|
function int(s) {
|
|
return v(parseInt(s));
|
|
}
|
|
|
|
function float(s) {
|
|
return v(parseFloat(s));
|
|
}
|
|
|
|
function abs(x) {
|
|
return Math.abs(v(x));
|
|
}
|
|
|
|
function max(a, b) {
|
|
return Math.max(v(a), v(b));
|
|
}
|
|
|
|
function min(a, b) {
|
|
return Math.min(v(a), v(b));
|
|
}
|
|
|
|
function sin(x) {
|
|
return Math.sin(v(x));
|
|
}
|
|
|
|
function cos(x) {
|
|
return Math.cos(v(x));
|
|
}
|
|
|
|
function sqrt(x) {
|
|
return v(Math.sqrt(v(x)));
|
|
}
|
|
|
|
function AstroCheck() {
|
|
var date = Astronomy.MakeTime(new Date('1700-01-01T00:00:00Z'));
|
|
var stop = Astronomy.MakeTime(new Date('2200-01-01T00:00:00Z'));
|
|
var body, pos, hor, dt, j2000, ofdate, time;
|
|
const observer = new Astronomy.Observer(29, -81, 10);
|
|
|
|
console.log(`o ${observer.latitude.toFixed(6)} ${observer.longitude.toFixed(6)} ${observer.height.toFixed(6)}`);
|
|
|
|
dt = (10 + Math.PI/100); // 10.03141592... days; exercise different times of day
|
|
while (date.tt < stop.tt) {
|
|
time = Astronomy.MakeTime(date);
|
|
|
|
for (body in Astronomy.Body) {
|
|
if (body !== 'Moon') {
|
|
pos = Astronomy.HelioVector(body, date);
|
|
console.log(`v ${body} ${pos.t.tt.toFixed(16)} ${pos.x.toFixed(16)} ${pos.y.toFixed(16)} ${pos.z.toFixed(16)}`);
|
|
|
|
if (body !== 'Earth' && body !== 'EMB' && body !== 'SSB') {
|
|
j2000 = Astronomy.Equator(body, date, observer, false, false);
|
|
ofdate = Astronomy.Equator(body, date, observer, true, true);
|
|
hor = Astronomy.Horizon(date, observer, ofdate.ra, ofdate.dec);
|
|
console.log(`s ${body} ${time.tt.toFixed(16)} ${time.ut.toFixed(16)} ${j2000.ra.toFixed(16)} ${j2000.dec.toFixed(16)} ${j2000.dist.toFixed(16)} ${hor.azimuth.toFixed(16)} ${hor.altitude.toFixed(16)}`);
|
|
}
|
|
}
|
|
}
|
|
pos = Astronomy.GeoMoon(date);
|
|
console.log(`v GM ${pos.t.tt.toFixed(16)} ${pos.x.toFixed(16)} ${pos.y.toFixed(16)} ${pos.z.toFixed(16)}`);
|
|
|
|
j2000 = Astronomy.Equator('Moon', date, observer, false, false);
|
|
ofdate = Astronomy.Equator('Moon', date, observer, true, true);
|
|
hor = Astronomy.Horizon(date, observer, ofdate.ra, ofdate.dec);
|
|
console.log(`s GM ${time.tt.toFixed(16)} ${time.ut.toFixed(16)} ${j2000.ra.toFixed(16)} ${j2000.dec.toFixed(16)} ${j2000.dist.toFixed(16)} ${hor.azimuth.toFixed(16)} ${hor.altitude.toFixed(16)}`);
|
|
|
|
date = date.AddDays(dt);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
function MoonPhase() {
|
|
function LoadMoonPhaseData(filename) {
|
|
// Load known moon phase times from US Naval Observatory.
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trimRight().split('\n');
|
|
let data = [];
|
|
for (let row of lines) {
|
|
let token = row.split(' ');
|
|
data.push({quarter:int(token[0]), date:new Date(token[1])});
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function TestLongitudes(data) {
|
|
let max_arcmin = 0;
|
|
for (let row of data) {
|
|
let elong = v(Astronomy.MoonPhase(row.date));
|
|
let expected_elong = 90 * v(row.quarter);
|
|
let degree_error = abs(elong - expected_elong);
|
|
if (degree_error > 180) degree_error = 360 - degree_error;
|
|
let arcmin = 60 * degree_error;
|
|
max_arcmin = max(max_arcmin, arcmin);
|
|
}
|
|
console.log(`JS TestLongitudes: nrows = ${data.length}, max_arcmin = ${max_arcmin}`);
|
|
if (max_arcmin > 1.0) {
|
|
console.log(`JS TestLongitudes: EXCESSIVE ANGULAR ERROR`);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function SearchYear(year, data, index) {
|
|
const millis_per_minute = 60*1000;
|
|
const threshold_minutes = 2; // max tolerable prediction error in minutes
|
|
let date = new Date(Date.UTC(year, 0, 1));
|
|
let maxdiff = 0;
|
|
let count = 0;
|
|
let mq = Astronomy.SearchMoonQuarter(date);
|
|
while (index < data.length && data[index].date.getUTCFullYear() === year) {
|
|
|
|
// Verify that we found anything at all.
|
|
if (!mq) {
|
|
console.log(`JS SearchYear: could not find next moon quarter after ${date.toISOString()}`);
|
|
return 1;
|
|
}
|
|
|
|
// Verify that we found the correct quarter.
|
|
if (data[index].quarter !== mq.quarter) {
|
|
console.log(`JS SearchYear: predicted quarter ${mq.quarter} but correct is ${data[index].quarter} for ${date.toISOString()}`);
|
|
return 1;
|
|
}
|
|
|
|
// Verify that the date and time we found is very close to the correct answer.
|
|
// Calculate the discrepancy in minutes.
|
|
// This is appropriate because the "correct" answers are only given to the minute.
|
|
let diff = abs(mq.time.date - data[index].date) / millis_per_minute;
|
|
if (diff > threshold_minutes) {
|
|
console.log(`JS SearchYear: EXCESSIVE ERROR = ${diff.toFixed(3)} minutes, correct=${data[index].date.toISOString()}, calculated=${mq.time.toString()}`);
|
|
return 1;
|
|
}
|
|
maxdiff = max(maxdiff, diff);
|
|
|
|
++index;
|
|
++count;
|
|
mq = Astronomy.NextMoonQuarter(mq);
|
|
date = mq.time.date;
|
|
}
|
|
if (Verbose) console.log(`JS SearchYear(${year}): count=${count}, maxdiff=${maxdiff.toFixed(3)}`);
|
|
return 0;
|
|
}
|
|
|
|
function TestSearch(data) {
|
|
// Search each year finding each quarter moon phase and confirm
|
|
// they match up with the correct answers stored in the 'data' array.
|
|
let index = 0; // index into 'data' for the current year we are interested in.
|
|
for (let year=1800; year <= 2100; year += 10) {
|
|
while (data[index].date.getUTCFullYear() < year) ++index;
|
|
let error = SearchYear(year, data, index);
|
|
if (error) return error;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function Statistics(data) {
|
|
// Figure out mean, min, and max differences between various moon quarters.
|
|
const stats = [null, null, null, null];
|
|
let prev = null;
|
|
for (let curr of data) {
|
|
if (prev && curr.date.getUTCFullYear() === prev.date.getUTCFullYear()) {
|
|
let dt = (curr.date - prev.date) / (24 * 3600 * 1000);
|
|
let s = stats[prev.quarter];
|
|
if (s) {
|
|
s.min = min(s.min, dt);
|
|
s.max = max(s.max, dt);
|
|
s.sum += dt;
|
|
++s.count;
|
|
} else {
|
|
stats[prev.quarter] = {
|
|
min: dt,
|
|
max: dt,
|
|
sum: dt,
|
|
count: 1
|
|
};
|
|
}
|
|
}
|
|
prev = curr;
|
|
}
|
|
|
|
for (let q=0; q < 4; ++q) {
|
|
let s = stats[q];
|
|
if (s) {
|
|
console.log(`JS MoonPhase statistics: q=${q} min=${s.min.toFixed(3)} avg=${(s.sum/s.count).toFixed(3)} max=${s.max.toFixed(3)} span=${(s.max-s.min).toFixed(3)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const TestData = LoadMoonPhaseData('moonphase/moonphases.txt');
|
|
if (Verbose) {
|
|
Statistics(TestData);
|
|
}
|
|
if (TestLongitudes(TestData)) return 1;
|
|
if (TestSearch(TestData)) return 1;
|
|
console.log('JS MoonPhase: PASS');
|
|
return 0;
|
|
}
|
|
|
|
|
|
function LunarApsis() {
|
|
const filename = 'apsides/moon.txt';
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.split(/\r?\n/);
|
|
|
|
const time_before = new Date();
|
|
let evt = Astronomy.SearchLunarApsis(new Date(Date.UTC(2001, 0, 1)));
|
|
|
|
let lnum = 0;
|
|
let count = 0;
|
|
let max_minute_error = 0;
|
|
let max_dist_error = 0;
|
|
for (let line of lines) {
|
|
++lnum;
|
|
|
|
let token = line.split(/\s+/);
|
|
if (token.length !== 3)
|
|
continue;
|
|
|
|
let kind = int(token[0]);
|
|
let date = new Date(token[1]);
|
|
let dist = int(token[2]);
|
|
|
|
if (evt.kind !== kind) {
|
|
console.log('line = ', line);
|
|
throw `${filename} line ${lnum}: Expected apsis type ${kind}, found ${evt.kind}`;
|
|
}
|
|
|
|
let diff_minutes = abs(evt.time.date - date) / (1000 * 60);
|
|
if (diff_minutes > 35) {
|
|
throw `${filename} line ${lnum}: Excessive time error: ${diff_minutes} minutes`;
|
|
}
|
|
max_minute_error = max(max_minute_error, diff_minutes);
|
|
|
|
let diff_dist = abs(evt.dist_km - dist);
|
|
if (diff_dist > 25) {
|
|
throw `${filename} line ${lnum}: Excessive distance error: ${diff_dist} km`;
|
|
}
|
|
max_dist_error = max(max_dist_error, diff_dist);
|
|
|
|
++count;
|
|
evt = Astronomy.NextLunarApsis(evt);
|
|
}
|
|
const time_after = new Date();
|
|
const elapsed = (time_after - time_before) / 1000;
|
|
|
|
if (Verbose) {
|
|
console.log(`JS LunarApsis: verified ${count} lines, max time error = ${max_minute_error.toFixed(3)} minutes, max dist error = ${max_dist_error.toFixed(3)} km.`);
|
|
}
|
|
|
|
if (count !== 2651)
|
|
throw 'FATAL: Did not process the expected number of data rows!';
|
|
|
|
console.log(`JS LunarApsis PASS: time=${elapsed.toFixed(3)}`);
|
|
return 0;
|
|
}
|
|
|
|
|
|
function LunarEclipseIssue78() {
|
|
// https://github.com/cosinekitty/astronomy/issues/78
|
|
|
|
let eclipse = Astronomy.SearchLunarEclipse(new Date(Date.UTC(2020, 11, 19)));
|
|
const expected_peak = new Date('2021-05-26T11:18:42Z'); // https://www.timeanddate.com/eclipse/lunar/2021-may-26
|
|
const dt = (expected_peak - eclipse.peak.date) / 1000;
|
|
if (abs(dt) > 40.0)
|
|
throw `LunarEclipseIssue78: Excessive prediction error = ${dt} seconds.`;
|
|
if (eclipse.kind !== 'total')
|
|
throw `Expected total eclipse; found: ${eclipse.kind}`;
|
|
console.log(`JS LunarEclipseIssue78: PASS`);
|
|
return 0;
|
|
}
|
|
|
|
|
|
function LunarEclipse() {
|
|
Astronomy.CalcMoonCount = 0;
|
|
const filename = 'eclipse/lunar_eclipse.txt';
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trim().split(/\r?\n/);
|
|
const diff_limit = 2.0;
|
|
let lnum = 0;
|
|
let skip_count = 0;
|
|
let max_diff_minutes = 0;
|
|
let sum_diff_minutes = 0;
|
|
let diff_count = 0;
|
|
let eclipse = Astronomy.SearchLunarEclipse(new Date(Date.UTC(1701, 0)));
|
|
for (let line of lines) {
|
|
++lnum;
|
|
if (line.length < 17) {
|
|
console.error(`JS TestLunarEclipse(${filename} line ${lnum}): line is too short.`);
|
|
return 1;
|
|
}
|
|
const time_text = line.substr(0, 17);
|
|
const peak_time = Astronomy.MakeTime(new Date(time_text));
|
|
const token = line.substr(17).trim().split(/\s+/);
|
|
if (token.length !== 2) {
|
|
console.error(`JS TestLunarEclipse(${filename} line ${lnum}): invalid number of tokens.`);
|
|
return 1;
|
|
}
|
|
const partial_minutes = float(token[0]);
|
|
const total_minutes = float(token[1]);
|
|
|
|
let valid = false;
|
|
switch (eclipse.kind) {
|
|
case 'penumbral':
|
|
valid = (eclipse.sd_penum > 0.0) && (eclipse.sd_partial == 0.0) && (eclipse.sd_total == 0.0);
|
|
break;
|
|
|
|
case 'partial':
|
|
valid = (eclipse.sd_penum > 0.0) && (eclipse.sd_partial > 0.0) && (eclipse.sd_total == 0.0);
|
|
break;
|
|
|
|
case 'total':
|
|
valid = (eclipse.sd_penum > 0.0) && (eclipse.sd_partial > 0.0) && (eclipse.sd_total > 0.0);
|
|
break;
|
|
|
|
default:
|
|
console.error(`JS LunarEclipseTest(${filename} line ${lnum}): invalid eclipse kind ${eclipse.kind}.`);
|
|
return 1;
|
|
}
|
|
|
|
if (!valid) {
|
|
console.error(`JS LunarEclipseTest(${filename} line ${lnum}): invalid semidurations for kind ${kind}: penum=${sd_penum}, partial=${sd_partial}, total=${sd_total}.`);
|
|
return 1;
|
|
}
|
|
|
|
// Check eclipse peak.
|
|
let diff_days = v(eclipse.peak.ut) - v(peak_time.ut);
|
|
|
|
// Tolerate missing penumbral eclipses - skip to next input line without calculating next eclipse.
|
|
if (partial_minutes == 0.0 && diff_days > 20.0) {
|
|
++skip_count;
|
|
continue;
|
|
}
|
|
|
|
let diff_minutes = (24.0 * 60.0) * abs(diff_days);
|
|
sum_diff_minutes += diff_minutes;
|
|
++diff_count;
|
|
|
|
if (diff_minutes > diff_limit) {
|
|
console.error(`JS LunarEclipseTest expected peak: ${peak_time}`);
|
|
console.error(`JS LunarEclipseTest found peak: ${eclipse.peak}`);
|
|
console.error(`JS LunarEclipseTest(${filename} line ${lnum}): EXCESSIVE peak time error = ${diff_minutes} minutes (${diff_days} days).`);
|
|
return 1;
|
|
}
|
|
|
|
if (diff_minutes > max_diff_minutes)
|
|
max_diff_minutes = diff_minutes;
|
|
|
|
/* check partial eclipse duration */
|
|
|
|
diff_minutes = abs(partial_minutes - eclipse.sd_partial);
|
|
sum_diff_minutes += diff_minutes;
|
|
++diff_count;
|
|
|
|
if (diff_minutes > diff_limit) {
|
|
console.error(`JS LunarEclipseTest(${filename} line ${lnum}): EXCESSIVE partial eclipse semiduration error: ${diff_minutes} minutes.`);
|
|
return 1;
|
|
}
|
|
|
|
if (diff_minutes > max_diff_minutes)
|
|
max_diff_minutes = diff_minutes;
|
|
|
|
/* check total eclipse duration */
|
|
|
|
diff_minutes = abs(total_minutes - eclipse.sd_total);
|
|
sum_diff_minutes += diff_minutes;
|
|
++diff_count;
|
|
|
|
if (diff_minutes > diff_limit) {
|
|
console.error(`JS LunarEclipseTest(${filename} line ${lnum}): EXCESSIVE total eclipse semiduration error: ${diff_minutes} minutes.`);
|
|
return 1;
|
|
}
|
|
|
|
if (diff_minutes > max_diff_minutes)
|
|
max_diff_minutes = diff_minutes;
|
|
|
|
/* calculate for next iteration */
|
|
|
|
eclipse = Astronomy.NextLunarEclipse(eclipse.peak);
|
|
}
|
|
console.log(`JS LunarEclipseTest: PASS (verified ${lnum}, skipped ${skip_count}, max_diff_minutes = ${max_diff_minutes}, avg_diff_minutes = ${sum_diff_minutes / diff_count}, moon calcs = ${Astronomy.CalcMoonCount})`);
|
|
return 0;
|
|
}
|
|
|
|
function VectorFromAngles(lat, lon) {
|
|
const coslat = cos(DEG2RAD * lat);
|
|
return [
|
|
cos(DEG2RAD * lon) * coslat,
|
|
sin(DEG2RAD * lon) * coslat,
|
|
sin(DEG2RAD * lat)
|
|
];
|
|
}
|
|
|
|
|
|
function AngleDiff(alat, alon, blat, blon) {
|
|
const a = VectorFromAngles(alat, alon);
|
|
const b = VectorFromAngles(blat, blon);
|
|
const dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
|
|
if (dot <= -1.0) {
|
|
return 180.0;
|
|
}
|
|
if (dot >= +1.0) {
|
|
return 0.0;
|
|
}
|
|
return v(RAD2DEG * Math.acos(dot));
|
|
}
|
|
|
|
|
|
function GlobalSolarEclipse() {
|
|
const expected_count = 1180;
|
|
const filename = 'eclipse/solar_eclipse.txt';
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trim().split(/\r?\n/);
|
|
let max_minutes = 0.0;
|
|
let max_angle = 0.0;
|
|
let skip_count = 0;
|
|
let eclipse = Astronomy.SearchGlobalSolarEclipse(new Date(Date.UTC(1701, 0)));
|
|
let lnum = 0;
|
|
for (let line of lines) {
|
|
++lnum;
|
|
// 1889-12-22T12:54:15Z -6 T -12.7 -12.8
|
|
let token = line.trim().split(/\s+/);
|
|
if (token.length !== 5) {
|
|
console.error(`JS GlobalSolarEclipse(${filename} line ${lnum}): invalid token count = ${token.length}`);
|
|
return 1;
|
|
}
|
|
const peak = Astronomy.MakeTime(new Date(token[0]));
|
|
const typeChar = token[2];
|
|
const lat = float(token[3]);
|
|
const lon = float(token[4]);
|
|
const expected_kind = {
|
|
'P': 'partial',
|
|
'A': 'annular',
|
|
'T': 'total',
|
|
'H': 'total'
|
|
}[typeChar];
|
|
|
|
let diff_days = eclipse.peak.tt - peak.tt;
|
|
// Sometimes we find marginal eclipses that aren't listed in the test data.
|
|
// Ignore them if the distance between the Sun/Moon shadow axis and the Earth's center is large.
|
|
while (diff_days < -25.0 && eclipse.distance > 9000.0) {
|
|
++skip_count;
|
|
eclipse = Astronomy.NextGlobalSolarEclipse(eclipse.peak);
|
|
diff_days = eclipse.peak.ut - peak.ut;
|
|
}
|
|
|
|
// Validate the eclipse prediction.
|
|
const diff_minutes = (24 * 60) * abs(diff_days);
|
|
if (diff_minutes > 6.93) {
|
|
console.error(`JS GlobalSolarEclipse(${filename} line ${lnum}): EXCESSIVE TIME ERROR = ${diff_minutes} minutes`);
|
|
return 1;
|
|
}
|
|
|
|
if (diff_minutes > max_minutes) {
|
|
max_minutes = diff_minutes;
|
|
}
|
|
|
|
// Validate the eclipse kind, but only when it is not a "glancing" eclipse.
|
|
if ((eclipse.distance < 6360) && (eclipse.kind != expected_kind)) {
|
|
console.error(`JS GlobalSolarEclipse(${filename} line ${lnum}): WRONG ECLIPSE KIND: expected ${expected_kind}, found ${eclipse.kind}`);
|
|
return 1;
|
|
}
|
|
|
|
if (eclipse.kind === 'total' || eclipse.kind === 'annular') {
|
|
// When the distance between the Moon's shadow ray and the Earth's center is beyond 6100 km,
|
|
// it creates a glancing blow whose geographic coordinates are excessively sensitive to
|
|
// slight changes in the ray. Therefore, it is unreasonable to count large errors there.
|
|
if (eclipse.distance < 6100.0) {
|
|
const diff_angle = AngleDiff(lat, lon, eclipse.latitude, eclipse.longitude);
|
|
if (diff_angle > 0.247) {
|
|
console.error(`JS GlobalSolarEclipse(${filename} line ${lnum}): EXCESSIVE GEOGRAPHIC LOCATION ERROR = ${diff_angle} degrees`);
|
|
console.log(` calculated lat=${eclipse.latitude}, lon=${eclipse.longitude}; expected ${lat}, ${lon}`);
|
|
return 1;
|
|
}
|
|
if (diff_angle > max_angle) {
|
|
max_angle = diff_angle;
|
|
}
|
|
}
|
|
}
|
|
|
|
eclipse = Astronomy.NextGlobalSolarEclipse(eclipse.peak);
|
|
}
|
|
|
|
if (lnum != expected_count) {
|
|
console.error(`JS GlobalSolarEclipse: WRONG LINE COUNT = ${lnum}, expected ${expected_count}`);
|
|
return 1;
|
|
}
|
|
|
|
if (skip_count > 2) {
|
|
console.error(`JS GlobalSolarEclipse: EXCESSIVE SKIP COUNT = ${skip_count}`);
|
|
return 1;
|
|
}
|
|
|
|
console.log(`JS GlobalSolarEclipse: PASS (${lnum} verified, ${skip_count} skipped, max minutes = ${max_minutes}, max angle = ${max_angle})`);
|
|
return 0;
|
|
}
|
|
|
|
|
|
function LocalSolarEclipse1() {
|
|
// Re-use the test data for global solar eclipses, only feed the given coordinates
|
|
// into the local solar eclipse predictor as the observer's location.
|
|
// In each case, start the search 20 days before the expected eclipse.
|
|
// Then verify that the peak time and eclipse type is correct in each case.
|
|
|
|
const expected_count = 1180;
|
|
const filename = 'eclipse/solar_eclipse.txt';
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trim().split(/\r?\n/);
|
|
let max_minutes = 0.0;
|
|
let skip_count = 0;
|
|
let lnum = 0;
|
|
for (let line of lines) {
|
|
++lnum;
|
|
// 1889-12-22T12:54:15Z -6 T -12.7 -12.8
|
|
let token = line.trim().split(/\s+/);
|
|
if (token.length !== 5) {
|
|
console.error(`JS LocalSolarEclipse1(${filename} line ${lnum}): invalid token count = ${token.length}`);
|
|
return 1;
|
|
}
|
|
const peak = Astronomy.MakeTime(new Date(token[0]));
|
|
//const typeChar = token[2];
|
|
const lat = float(token[3]);
|
|
const lon = float(token[4]);
|
|
const observer = new Astronomy.Observer(lat, lon, 0);
|
|
|
|
// Start the search 20 days before we know the eclipse should peak.
|
|
const search_start = peak.AddDays(-20);
|
|
const eclipse = Astronomy.SearchLocalSolarEclipse(search_start, observer);
|
|
|
|
// Validate the predicted eclipse peak time.
|
|
const diff_days = v(eclipse.peak.time.tt) - v(peak.tt);
|
|
if (diff_days > 20) {
|
|
++skip_count;
|
|
continue;
|
|
}
|
|
|
|
const diff_minutes = (24 * 60) * abs(diff_days);
|
|
if (diff_minutes > 7.14) {
|
|
console.error(`JS LocalSolarEclipse1(${filename} line ${lnum}): EXCESSIVE TIME ERROR = ${diff_minutes} minutes`);
|
|
return 1;
|
|
}
|
|
|
|
if (diff_minutes > max_minutes) {
|
|
max_minutes = diff_minutes;
|
|
}
|
|
|
|
// Confirm that we can call NextLocalSolarEclipse and it doesn't throw an exception.
|
|
// Could add more stringent checking here later.
|
|
const next = Astronomy.NextLocalSolarEclipse(eclipse.peak.time, observer);
|
|
v(next.peak.time.tt);
|
|
}
|
|
|
|
if (lnum != expected_count) {
|
|
console.error(`JS LocalSolarEclipse1: WRONG LINE COUNT = ${lnum}, expected ${expected_count}`);
|
|
return 1;
|
|
}
|
|
|
|
if (skip_count > 6) {
|
|
console.error(`JS LocalSolarEclipse1: EXCESSIVE SKIP COUNT = ${skip_count}`);
|
|
return 1;
|
|
}
|
|
|
|
console.log(`JS LocalSolarEclipse1: PASS (${lnum} verified, ${skip_count} skipped, max minutes = ${max_minutes})`);
|
|
return 0;
|
|
}
|
|
|
|
|
|
function TrimLine(line) {
|
|
// Treat '#' as a comment character.
|
|
const poundIndex = line.indexOf('#');
|
|
if (poundIndex >= 0) {
|
|
line = line.substr(0, poundIndex);
|
|
}
|
|
line = line.trim();
|
|
return line;
|
|
}
|
|
|
|
|
|
function ParseEvent(time_str, alt_str, required) {
|
|
if (required) {
|
|
return {
|
|
time: new Astronomy.MakeTime(new Date(time_str)),
|
|
altitude: float(alt_str)
|
|
};
|
|
}
|
|
if (time_str !== '-') {
|
|
throw `Expected event time to be "-" but found "${time_str}"`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
function LocalSolarEclipse2() {
|
|
// Test ability to calculate local solar eclipse conditions away from
|
|
// the peak position on the Earth.
|
|
|
|
const filename = 'eclipse/local_solar_eclipse.txt';
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trim().split(/\r?\n/);
|
|
let lnum = 0;
|
|
let verify_count = 0;
|
|
let max_minutes = 0.0;
|
|
let max_degrees = 0.0;
|
|
|
|
function CheckEvent(calc, expect) {
|
|
const diff_minutes = (24 * 60) * abs(expect.time.ut - calc.time.ut);
|
|
if (diff_minutes > max_minutes) {
|
|
max_minutes = diff_minutes;
|
|
}
|
|
if (diff_minutes > 1.0) {
|
|
throw `CheckEvent(${filename} line ${lnum}): EXCESSIVE TIME ERROR: ${diff_minutes} minutes.`;
|
|
}
|
|
const diff_alt = abs(expect.altitude - calc.altitude);
|
|
if (diff_alt > max_degrees) {
|
|
max_degrees = diff_alt;
|
|
}
|
|
if (diff_alt > 0.5) {
|
|
throw `CheckEvent(${filename} line ${lnum}): EXCESSIVE ALTITUDE ERROR: ${diff_alt} degrees.`;
|
|
}
|
|
}
|
|
|
|
for (let line of lines) {
|
|
++lnum;
|
|
line = TrimLine(line);
|
|
if (line === '') continue;
|
|
const token = line.split(/\s+/);
|
|
if (token.length !== 13) {
|
|
console.error(`JS LocalSolarEclipse2(${filename} line ${lnum}): Incorrect token count = ${token.length}`);
|
|
return 1;
|
|
}
|
|
const latitude = float(token[0]);
|
|
const longitude = float(token[1]);
|
|
const observer = new Astronomy.Observer(latitude, longitude, 0);
|
|
const typeChar = token[2];
|
|
const expected_kind = {
|
|
'P': 'partial',
|
|
'A': 'annular',
|
|
'T': 'total',
|
|
'H': 'total'
|
|
}[typeChar];
|
|
const p1 = ParseEvent(token[3], token[4], true);
|
|
const t1 = ParseEvent(token[5], token[6], (typeChar !== 'P'));
|
|
const peak = ParseEvent(token[7], token[8], true);
|
|
const t2 = ParseEvent(token[9], token[10], (typeChar !== 'P'));
|
|
const p2 = ParseEvent(token[11], token[12], true);
|
|
|
|
const search_time = p1.time.AddDays(-20);
|
|
const eclipse = Astronomy.SearchLocalSolarEclipse(search_time, observer);
|
|
if (eclipse.kind !== expected_kind) {
|
|
console.error(`JS LocalSolarEclipse2(${filename} line ${lnum}): expected eclipse kind "${expected_kind}" but found "${eclipse.kind}".`);
|
|
return 1;
|
|
}
|
|
CheckEvent(eclipse.peak, peak);
|
|
CheckEvent(eclipse.partial_begin, p1);
|
|
CheckEvent(eclipse.partial_end, p2);
|
|
if (typeChar != 'P') {
|
|
CheckEvent(eclipse.total_begin, t1);
|
|
CheckEvent(eclipse.total_end, t2);
|
|
}
|
|
++verify_count;
|
|
}
|
|
|
|
console.log(`JS LocalSolarEclipse2: PASS (${verify_count} verified, max_minutes = ${max_minutes}, max_degrees = ${max_degrees})`);
|
|
return 0;
|
|
}
|
|
|
|
|
|
function LocalSolarEclipse() {
|
|
if (0 !== LocalSolarEclipse1())
|
|
return 1;
|
|
|
|
if (0 !== LocalSolarEclipse2())
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
function PlanetApsis() {
|
|
const Planet = {
|
|
Mercury: { OrbitalPeriod: 87.969 },
|
|
Venus: { OrbitalPeriod: 224.701 },
|
|
Earth: { OrbitalPeriod: 365.256 },
|
|
Mars: { OrbitalPeriod: 686.980 },
|
|
Jupiter: { OrbitalPeriod: 4332.589 },
|
|
Saturn: { OrbitalPeriod: 10759.22 },
|
|
Uranus: { OrbitalPeriod: 30685.4 },
|
|
Neptune: { OrbitalPeriod: 60189.0 },
|
|
Pluto: { OrbitalPeriod: 90560.0 }
|
|
};
|
|
const start_time = Astronomy.MakeTime(new Date('1700-01-01T00:00:00Z'));
|
|
let pindex = 0;
|
|
for (let body of ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto']) {
|
|
let count = 1;
|
|
const period = Planet[body].OrbitalPeriod;
|
|
let min_interval = -1.0;
|
|
let max_interval = -1.0;
|
|
let max_diff_days = 0.0;
|
|
let max_dist_ratio = 0.0;
|
|
let apsis = Astronomy.SearchPlanetApsis(body, start_time);
|
|
const filename = `apsides/apsis_${pindex}.txt`;
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.split(/\r?\n/);
|
|
for (const line of lines) {
|
|
if (line.trim() == '') {
|
|
continue;
|
|
}
|
|
const token = line.split(/\s+/);
|
|
if (token.length != 3) {
|
|
throw `${filename} line ${count}: Invalid data format: ${token.length} tokens.`;
|
|
}
|
|
const expected_kind = int(token[0]);
|
|
const expected_time = Astronomy.MakeTime(new Date(token[1]));
|
|
const expected_distance = float(token[2]);
|
|
if (expected_kind !== apsis.kind) {
|
|
throw `${filename} line ${count}: WRONG APSIS KIND: expected ${expected_kind}, found ${apsis.kind}`;
|
|
}
|
|
const diff_days = abs(expected_time.tt - apsis.time.tt);
|
|
max_diff_days = max(max_diff_days, diff_days);
|
|
const diff_degrees = (diff_days / period) * 360;
|
|
const degree_threshold = (body === 'Pluto') ? 0.262 : 0.1;
|
|
if (diff_degrees > degree_threshold) {
|
|
throw `APSIS FAIL: ${body} exceeded angular threshold (${diff_degrees} vs ${degree_threshold} degrees).`;
|
|
}
|
|
const diff_dist_ratio = abs(expected_distance - apsis.dist_au) / expected_distance;
|
|
max_dist_ratio = max(max_dist_ratio, diff_dist_ratio);
|
|
if (diff_dist_ratio > 1.05e-4) {
|
|
throw `${filename} line ${count}: distance ratio ${diff_dist_ratio} is too large.`;
|
|
}
|
|
|
|
// Calculate the next apsis
|
|
const prev_time = apsis.time;
|
|
apsis = Astronomy.NextPlanetApsis(body, apsis);
|
|
++count;
|
|
const interval = apsis.time.tt - prev_time.tt;
|
|
if (min_interval < 0.0) {
|
|
min_interval = max_interval = interval;
|
|
} else {
|
|
min_interval = min(min_interval, interval);
|
|
max_interval = max(max_interval, interval);
|
|
}
|
|
}
|
|
if (count < 2) {
|
|
throw `Failed to find apsides for ${body}`;
|
|
}
|
|
if (Verbose) console.log(`JS PlanetApsis: ${count} apsides for ${body} -- intervals: min=${min_interval}, max=${max_interval}, ratio=${max_interval/min_interval}; max day=${max_diff_days}, degrees=${(max_diff_days / period) * 360}, dist ratio=${max_dist_ratio}`);
|
|
++pindex;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
function Elongation() {
|
|
function SearchElongTest() {
|
|
// Max elongation data obtained from:
|
|
// http://www.skycaramba.com/greatest_elongations.shtml
|
|
TestMaxElong('Mercury', '2010-01-17T05:22Z', '2010-01-27T05:22Z', 24.80, 'morning');
|
|
TestMaxElong('Mercury', '2010-05-16T02:15Z', '2010-05-26T02:15Z', 25.10, 'morning');
|
|
TestMaxElong('Mercury', '2010-09-09T17:24Z', '2010-09-19T17:24Z', 17.90, 'morning');
|
|
TestMaxElong('Mercury', '2010-12-30T14:33Z', '2011-01-09T14:33Z', 23.30, 'morning');
|
|
TestMaxElong('Mercury', '2011-04-27T19:03Z', '2011-05-07T19:03Z', 26.60, 'morning');
|
|
TestMaxElong('Mercury', '2011-08-24T05:52Z', '2011-09-03T05:52Z', 18.10, 'morning');
|
|
TestMaxElong('Mercury', '2011-12-13T02:56Z', '2011-12-23T02:56Z', 21.80, 'morning');
|
|
TestMaxElong('Mercury', '2012-04-08T17:22Z', '2012-04-18T17:22Z', 27.50, 'morning');
|
|
TestMaxElong('Mercury', '2012-08-06T12:04Z', '2012-08-16T12:04Z', 18.70, 'morning');
|
|
TestMaxElong('Mercury', '2012-11-24T22:55Z', '2012-12-04T22:55Z', 20.60, 'morning');
|
|
TestMaxElong('Mercury', '2013-03-21T22:02Z', '2013-03-31T22:02Z', 27.80, 'morning');
|
|
TestMaxElong('Mercury', '2013-07-20T08:51Z', '2013-07-30T08:51Z', 19.60, 'morning');
|
|
TestMaxElong('Mercury', '2013-11-08T02:28Z', '2013-11-18T02:28Z', 19.50, 'morning');
|
|
TestMaxElong('Mercury', '2014-03-04T06:38Z', '2014-03-14T06:38Z', 27.60, 'morning');
|
|
TestMaxElong('Mercury', '2014-07-02T18:22Z', '2014-07-12T18:22Z', 20.90, 'morning');
|
|
TestMaxElong('Mercury', '2014-10-22T12:36Z', '2014-11-01T12:36Z', 18.70, 'morning');
|
|
TestMaxElong('Mercury', '2015-02-14T16:20Z', '2015-02-24T16:20Z', 26.70, 'morning');
|
|
TestMaxElong('Mercury', '2015-06-14T17:10Z', '2015-06-24T17:10Z', 22.50, 'morning');
|
|
TestMaxElong('Mercury', '2015-10-06T03:20Z', '2015-10-16T03:20Z', 18.10, 'morning');
|
|
TestMaxElong('Mercury', '2016-01-28T01:22Z', '2016-02-07T01:22Z', 25.60, 'morning');
|
|
TestMaxElong('Mercury', '2016-05-26T08:45Z', '2016-06-05T08:45Z', 24.20, 'morning');
|
|
TestMaxElong('Mercury', '2016-09-18T19:27Z', '2016-09-28T19:27Z', 17.90, 'morning');
|
|
TestMaxElong('Mercury', '2017-01-09T09:42Z', '2017-01-19T09:42Z', 24.10, 'morning');
|
|
TestMaxElong('Mercury', '2017-05-07T23:19Z', '2017-05-17T23:19Z', 25.80, 'morning');
|
|
TestMaxElong('Mercury', '2017-09-02T10:14Z', '2017-09-12T10:14Z', 17.90, 'morning');
|
|
TestMaxElong('Mercury', '2017-12-22T19:48Z', '2018-01-01T19:48Z', 22.70, 'morning');
|
|
TestMaxElong('Mercury', '2018-04-19T18:17Z', '2018-04-29T18:17Z', 27.00, 'morning');
|
|
TestMaxElong('Mercury', '2018-08-16T20:35Z', '2018-08-26T20:35Z', 18.30, 'morning');
|
|
TestMaxElong('Mercury', '2018-12-05T11:34Z', '2018-12-15T11:34Z', 21.30, 'morning');
|
|
TestMaxElong('Mercury', '2019-04-01T19:40Z', '2019-04-11T19:40Z', 27.70, 'morning');
|
|
TestMaxElong('Mercury', '2019-07-30T23:08Z', '2019-08-09T23:08Z', 19.00, 'morning');
|
|
TestMaxElong('Mercury', '2019-11-18T10:31Z', '2019-11-28T10:31Z', 20.10, 'morning');
|
|
TestMaxElong('Mercury', '2010-03-29T23:32Z', '2010-04-08T23:32Z', 19.40, 'evening');
|
|
TestMaxElong('Mercury', '2010-07-28T01:03Z', '2010-08-07T01:03Z', 27.40, 'evening');
|
|
TestMaxElong('Mercury', '2010-11-21T15:42Z', '2010-12-01T15:42Z', 21.50, 'evening');
|
|
TestMaxElong('Mercury', '2011-03-13T01:07Z', '2011-03-23T01:07Z', 18.60, 'evening');
|
|
TestMaxElong('Mercury', '2011-07-10T04:56Z', '2011-07-20T04:56Z', 26.80, 'evening');
|
|
TestMaxElong('Mercury', '2011-11-04T08:40Z', '2011-11-14T08:40Z', 22.70, 'evening');
|
|
TestMaxElong('Mercury', '2012-02-24T09:39Z', '2012-03-05T09:39Z', 18.20, 'evening');
|
|
TestMaxElong('Mercury', '2012-06-21T02:00Z', '2012-07-01T02:00Z', 25.70, 'evening');
|
|
TestMaxElong('Mercury', '2012-10-16T21:59Z', '2012-10-26T21:59Z', 24.10, 'evening');
|
|
TestMaxElong('Mercury', '2013-02-06T21:24Z', '2013-02-16T21:24Z', 18.10, 'evening');
|
|
TestMaxElong('Mercury', '2013-06-02T16:45Z', '2013-06-12T16:45Z', 24.30, 'evening');
|
|
TestMaxElong('Mercury', '2013-09-29T09:59Z', '2013-10-09T09:59Z', 25.30, 'evening');
|
|
TestMaxElong('Mercury', '2014-01-21T10:00Z', '2014-01-31T10:00Z', 18.40, 'evening');
|
|
TestMaxElong('Mercury', '2014-05-15T07:06Z', '2014-05-25T07:06Z', 22.70, 'evening');
|
|
TestMaxElong('Mercury', '2014-09-11T22:20Z', '2014-09-21T22:20Z', 26.40, 'evening');
|
|
TestMaxElong('Mercury', '2015-01-04T20:26Z', '2015-01-14T20:26Z', 18.90, 'evening');
|
|
TestMaxElong('Mercury', '2015-04-27T04:46Z', '2015-05-07T04:46Z', 21.20, 'evening');
|
|
TestMaxElong('Mercury', '2015-08-25T10:20Z', '2015-09-04T10:20Z', 27.10, 'evening');
|
|
TestMaxElong('Mercury', '2015-12-19T03:11Z', '2015-12-29T03:11Z', 19.70, 'evening');
|
|
TestMaxElong('Mercury', '2016-04-08T14:00Z', '2016-04-18T14:00Z', 19.90, 'evening');
|
|
TestMaxElong('Mercury', '2016-08-06T21:24Z', '2016-08-16T21:24Z', 27.40, 'evening');
|
|
TestMaxElong('Mercury', '2016-12-01T04:36Z', '2016-12-11T04:36Z', 20.80, 'evening');
|
|
TestMaxElong('Mercury', '2017-03-22T10:24Z', '2017-04-01T10:24Z', 19.00, 'evening');
|
|
TestMaxElong('Mercury', '2017-07-20T04:34Z', '2017-07-30T04:34Z', 27.20, 'evening');
|
|
TestMaxElong('Mercury', '2017-11-14T00:32Z', '2017-11-24T00:32Z', 22.00, 'evening');
|
|
TestMaxElong('Mercury', '2018-03-05T15:07Z', '2018-03-15T15:07Z', 18.40, 'evening');
|
|
TestMaxElong('Mercury', '2018-07-02T05:24Z', '2018-07-12T05:24Z', 26.40, 'evening');
|
|
TestMaxElong('Mercury', '2018-10-27T15:25Z', '2018-11-06T15:25Z', 23.30, 'evening');
|
|
TestMaxElong('Mercury', '2019-02-17T01:23Z', '2019-02-27T01:23Z', 18.10, 'evening');
|
|
TestMaxElong('Mercury', '2019-06-13T23:14Z', '2019-06-23T23:14Z', 25.20, 'evening');
|
|
TestMaxElong('Mercury', '2019-10-10T04:00Z', '2019-10-20T04:00Z', 24.60, 'evening');
|
|
TestMaxElong('Venus', '2010-12-29T15:57Z', '2011-01-08T15:57Z', 47.00, 'morning');
|
|
TestMaxElong('Venus', '2012-08-05T08:59Z', '2012-08-15T08:59Z', 45.80, 'morning');
|
|
TestMaxElong('Venus', '2014-03-12T19:25Z', '2014-03-22T19:25Z', 46.60, 'morning');
|
|
TestMaxElong('Venus', '2015-10-16T06:57Z', '2015-10-26T06:57Z', 46.40, 'morning');
|
|
TestMaxElong('Venus', '2017-05-24T13:09Z', '2017-06-03T13:09Z', 45.90, 'morning');
|
|
TestMaxElong('Venus', '2018-12-27T04:24Z', '2019-01-06T04:24Z', 47.00, 'morning');
|
|
TestMaxElong('Venus', '2010-08-10T03:19Z', '2010-08-20T03:19Z', 46.00, 'evening');
|
|
TestMaxElong('Venus', '2012-03-17T08:03Z', '2012-03-27T08:03Z', 46.00, 'evening');
|
|
TestMaxElong('Venus', '2013-10-22T08:00Z', '2013-11-01T08:00Z', 47.10, 'evening');
|
|
TestMaxElong('Venus', '2015-05-27T18:46Z', '2015-06-06T18:46Z', 45.40, 'evening');
|
|
TestMaxElong('Venus', '2017-01-02T13:19Z', '2017-01-12T13:19Z', 47.10, 'evening');
|
|
TestMaxElong('Venus', '2018-08-07T17:02Z', '2018-08-17T17:02Z', 45.90, 'evening');
|
|
}
|
|
|
|
function LoadData(filename) {
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trimRight().split('\n');
|
|
let data = [];
|
|
for (let row of lines) {
|
|
let token = row.split(/\s+/);
|
|
data.push({date:new Date(token[0]), body:token[1]});
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function TestFile(filename, startSearchYear, targetRelLon) {
|
|
const data = LoadData(filename);
|
|
const startDate = new Date(Date.UTC(startSearchYear, 0, 1));
|
|
for (let item of data) {
|
|
let time = Astronomy.SearchRelativeLongitude(item.body, targetRelLon, startDate);
|
|
let diff_minutes = (time.date - item.date) / 60000;
|
|
if (Verbose) console.log(`JS ${item.body}: error = ${diff_minutes.toFixed(3)} minutes`);
|
|
if (abs(diff_minutes) > 15)
|
|
throw `!!! Excessive error for body ${item.body}`;
|
|
}
|
|
}
|
|
|
|
function TestPlanet(outFileName, body, startYear, stopYear, zeroLonEventName) {
|
|
let rlon = 0;
|
|
let date = new Date(Date.UTC(startYear, 0, 1));
|
|
let stopDate = new Date(Date.UTC(stopYear, 0, 1));
|
|
let text = '';
|
|
let count = 0;
|
|
let prev_time, min_diff, max_diff, sum_diff=0;
|
|
|
|
while (date < stopDate) {
|
|
let event = (rlon === 0) ? zeroLonEventName : 'sup';
|
|
let evt_time = Astronomy.SearchRelativeLongitude(body, rlon, date);
|
|
if (prev_time) {
|
|
// Check for consistent intervals.
|
|
// Mainly I don't want to accidentally skip over an event!
|
|
let day_diff = evt_time.tt - prev_time.tt;
|
|
if (min_diff === undefined) {
|
|
min_diff = max_diff = day_diff;
|
|
} else {
|
|
min_diff = min(min_diff, day_diff);
|
|
max_diff = max(max_diff, day_diff);
|
|
}
|
|
sum_diff += day_diff;
|
|
}
|
|
let geo = Astronomy.GeoVector(body, evt_time, false);
|
|
let dist = sqrt(geo.x*geo.x + geo.y*geo.y + geo.z*geo.z);
|
|
text += `e ${body} ${event} ${evt_time.tt} ${dist}\n`;
|
|
rlon = 180 - rlon;
|
|
date = evt_time.date;
|
|
++count;
|
|
prev_time = evt_time;
|
|
}
|
|
|
|
fs.writeFileSync(outFileName, text);
|
|
|
|
const ratio = max_diff / min_diff;
|
|
if (Verbose) console.log(`JS TestPlanet(${body}): ${count} events, interval min=${min_diff.toFixed(1)}, max=${max_diff.toFixed(1)}, avg=${(sum_diff/count).toFixed(1)}, ratio=${ratio.toFixed(3)}`);
|
|
|
|
let thresh = {Mercury:1.65, Mars:1.30}[body] || 1.07;
|
|
if (ratio > thresh)
|
|
throw `TestPlanet: Excessive event interval ratio for ${body} = ${ratio}`;
|
|
}
|
|
|
|
function TestMaxElong(body, startText, verifyText, verifyAngle, verifyVisibility) {
|
|
let startDate = new Date(startText);
|
|
let verifyDate = new Date(verifyText);
|
|
let evt = Astronomy.SearchMaxElongation(body, startDate);
|
|
|
|
let hour_diff = (verifyDate - evt.time.date) / (1000 * 3600);
|
|
let arcmin_diff = 60.0 * abs(evt.elongation - verifyAngle);
|
|
if (Verbose) console.log(`JS TestMaxElong: ${body.padStart(8)} ${evt.visibility.padStart(8)} elong=${evt.elongation.toFixed(2).padStart(5)} (${arcmin_diff.toFixed(2).padStart(4)} arcmin) ${evt.time.toString()} (err ${hour_diff.toFixed(2).padStart(5)} hours)`);
|
|
|
|
if (evt.visibility !== verifyVisibility)
|
|
throw `TestMaxElong: expected visibility ${verifyVisibility}, but found ${evt.visibility}`;
|
|
|
|
if (arcmin_diff > 4.0)
|
|
throw `TestMaxElong: excessive angular error = ${angle_diff} arcmin`;
|
|
|
|
if (abs(hour_diff) > 0.603)
|
|
throw `TestMaxElong: excessive hour error = ${hour_diff}`;
|
|
}
|
|
|
|
TestFile('longitude/opposition_2018.txt', 2018, 0);
|
|
|
|
for (let body of ['Mercury', 'Venus'])
|
|
TestPlanet(`temp/js_longitude_${body}.txt`, body, 1700, 2200, 'inf');
|
|
|
|
for (let body of ['Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto'])
|
|
TestPlanet(`temp/js_longitude_${body}.txt`, body, 1700, 2200, 'opp');
|
|
|
|
SearchElongTest();
|
|
|
|
console.log('JS Elongation: PASS');
|
|
return 0;
|
|
}
|
|
|
|
|
|
function Seasons() {
|
|
function LoadTestData(filename) {
|
|
// Moon 150 -45 2050-03-07T19:13Z s
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trimRight().split('\n');
|
|
let data = [];
|
|
let lnum = 0;
|
|
let minByMonth = [];
|
|
let maxByMonth = [];
|
|
for (let row of lines) {
|
|
let token = row.split(/\s+/g);
|
|
let item = {
|
|
lnum: ++lnum,
|
|
date: new Date(token[0]),
|
|
name: token[1] // Perihelion, Equinox, Solstice, Aphelion
|
|
};
|
|
data.push(item);
|
|
|
|
if (item.name === 'Equinox' || item.name == 'Solstice') {
|
|
let month = 1 + item.date.getUTCMonth();
|
|
let format = item.date.toISOString().substring(8);
|
|
if (!minByMonth[month] || format < minByMonth[month])
|
|
minByMonth[month] = format;
|
|
if (!maxByMonth[month] || format > maxByMonth[month])
|
|
maxByMonth[month] = format;
|
|
}
|
|
}
|
|
|
|
if (Verbose) {
|
|
console.log(`JS Seasons LoadTestData: count = ${data.length}`);
|
|
for (let month of [3, 6, 9, 12]) {
|
|
console.log(`Month ${month}: earliest ${minByMonth[month]}, latest ${maxByMonth[month]}`);
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
const data = LoadTestData('seasons/seasons.txt');
|
|
let current_year;
|
|
let seasons;
|
|
let calc_date;
|
|
let min_diff, max_diff, sum_diff = 0, count = 0;
|
|
let month_max_diff = [];
|
|
for (let item of data) {
|
|
let year = item.date.getUTCFullYear();
|
|
if (current_year !== year) {
|
|
current_year = year;
|
|
seasons = Astronomy.Seasons(year);
|
|
}
|
|
calc_date = null;
|
|
let month = 1 + item.date.getUTCMonth();
|
|
switch (item.name) {
|
|
case 'Equinox':
|
|
switch (month) {
|
|
case 3:
|
|
calc_date = seasons.mar_equinox.date;
|
|
break;
|
|
case 9:
|
|
calc_date = seasons.sep_equinox.date;
|
|
break;
|
|
default:
|
|
throw `ERROR: Invalid equinox date in test data: ${item.date.toISOString()}`;
|
|
}
|
|
break;
|
|
case 'Solstice':
|
|
switch (month) {
|
|
case 6:
|
|
calc_date = seasons.jun_solstice.date;
|
|
break;
|
|
case 12:
|
|
calc_date = seasons.dec_solstice.date;
|
|
break;
|
|
default:
|
|
throw `ERROR: Invalid solstice date in test data: ${item.date.toISOString()}`;
|
|
}
|
|
break;
|
|
default:
|
|
continue; // ignore the other kinds of events for now
|
|
}
|
|
if (!calc_date)
|
|
throw `ERROR: Missing calc_date for test date ${item.date.toISOString()}`;
|
|
let diff_minutes = (calc_date - item.date) / 60000;
|
|
if (abs(diff_minutes) > 2.37) {
|
|
throw `ERROR: Excessive error in season calculation: ${diff_minutes.toFixed(3)} minutes`;
|
|
}
|
|
|
|
if (min_diff === undefined) {
|
|
min_diff = max_diff = diff_minutes;
|
|
} else {
|
|
min_diff = min(min_diff, diff_minutes);
|
|
max_diff = max(max_diff, diff_minutes);
|
|
}
|
|
if (month_max_diff[month] === undefined) {
|
|
month_max_diff[month] = abs(diff_minutes);
|
|
} else {
|
|
month_max_diff[month] = max(month_max_diff[month], abs(diff_minutes));
|
|
}
|
|
sum_diff += diff_minutes;
|
|
++count;
|
|
}
|
|
|
|
if (Verbose) {
|
|
console.log(`JS Seasons: n=${count}, minute errors: min=${min_diff.toFixed(3)}, avg=${(sum_diff/count).toFixed(3)}, max=${max_diff.toFixed(3)}`);
|
|
for (let month of [3, 6, 9, 12]) {
|
|
console.log(`JS Seasons: max diff by month ${month} = ${month_max_diff[month].toFixed(3)}`);
|
|
}
|
|
}
|
|
console.log('JS Seasons: PASS');
|
|
}
|
|
|
|
|
|
function RiseSet() {
|
|
function LoadTestData(filename) {
|
|
// Moon 150 -45 2050-03-07T19:13Z s
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trimRight().split('\n');
|
|
let data = [];
|
|
let lnum = 0;
|
|
for (let row of lines) {
|
|
let token = row.split(/\s+/g);
|
|
data.push({
|
|
lnum: ++lnum,
|
|
body: token[0],
|
|
lon: float(token[1]),
|
|
lat: float(token[2]),
|
|
date: new Date(token[3]),
|
|
direction: { r:+1, s:-1 }[token[4]] || Fail(`Invalid event code ${token[4]}`)
|
|
});
|
|
}
|
|
return data;
|
|
}
|
|
const data = LoadTestData('riseset/riseset.txt');
|
|
|
|
// The test data is sorted by body, then geographic location, then date/time.
|
|
|
|
const before_date = new Date();
|
|
|
|
let body;
|
|
let observer;
|
|
let r_search_date, r_date;
|
|
let s_search_date, s_date;
|
|
let a_date, b_date, a_dir, b_dir;
|
|
let sum_minutes = 0;
|
|
let max_minutes = 0;
|
|
for (let evt of data) {
|
|
if (!observer || observer.latitude !== evt.lat || observer.longitude !== evt.lon || body !== evt.body) {
|
|
// Every time we see a new geographic location, start a new iteration
|
|
// of finding all rise/set times for that UTC calendar year.
|
|
body = evt.body;
|
|
observer = new Astronomy.Observer(evt.lat, evt.lon, 0);
|
|
r_search_date = s_search_date = new Date(Date.UTC(evt.date.getUTCFullYear(), 0, 1));
|
|
b_date = null;
|
|
if (Verbose) {
|
|
console.log(`JS RiseSet: ${body} ${evt.lat} ${evt.lon}`);
|
|
}
|
|
}
|
|
|
|
if (b_date) {
|
|
// recycle the second event from the previous iteration as the first event
|
|
a_date = b_date;
|
|
a_dir = b_dir;
|
|
b_date = null;
|
|
} else {
|
|
r_date = Astronomy.SearchRiseSet(body, observer, +1, r_search_date, 366) ||
|
|
Fail(`JS RiseSet: Did not find ${body} rise after ${r_search_date.toISOString()}`);
|
|
|
|
s_date = Astronomy.SearchRiseSet(body, observer, -1, s_search_date, 366) ||
|
|
Fail(`JS RiseSet: Did not find ${body} set after ${s_search_date.toISOString()}`);
|
|
|
|
// Expect the current event to match the earlier of the found dates.
|
|
if (r_date.tt < s_date.tt) {
|
|
a_date = r_date;
|
|
b_date = s_date;
|
|
a_dir = +1;
|
|
b_dir = -1;
|
|
} else {
|
|
a_date = s_date;
|
|
b_date = r_date;
|
|
a_dir = -1;
|
|
b_dir = +1;
|
|
}
|
|
|
|
r_search_date = r_date.AddDays(0.01).date;
|
|
s_search_date = s_date.AddDays(0.01).date;
|
|
}
|
|
|
|
if (a_dir !== evt.direction) {
|
|
Fail(`[line ${evt.lnum}] Expected ${body} dir=${evt.direction} at ${evt.date.toISOString()} but found ${a_dir} ${a_date.toString()}`);
|
|
}
|
|
|
|
let error_minutes = abs(a_date.date - evt.date) / 60000;
|
|
sum_minutes += error_minutes * error_minutes;
|
|
if (error_minutes > max_minutes) {
|
|
max_minutes = error_minutes;
|
|
if (Verbose) {
|
|
console.log(`Line ${evt.lnum} : error = ${error_minutes.toFixed(4)}`);
|
|
}
|
|
}
|
|
if (error_minutes > 0.57) {
|
|
console.log(`Expected ${evt.date.toISOString()}`);
|
|
console.log(`Found ${a_date.toString()}`);
|
|
Fail("Excessive prediction time error.");
|
|
}
|
|
}
|
|
|
|
const after_date = new Date();
|
|
const elapsed_seconds = (after_date - before_date) / 1000;
|
|
|
|
console.log(`JS RiseSet PASS: elapsed=${elapsed_seconds.toFixed(3)}, error in minutes: rms=${sqrt(sum_minutes/data.length).toFixed(4)}, max=${max_minutes.toFixed(4)}`);
|
|
return 0;
|
|
}
|
|
|
|
|
|
function Rotation() {
|
|
function CompareMatrices(caller, a, b, tolerance) {
|
|
for (let i=0; i<3; ++i) {
|
|
for (let j=0; j<3; ++j) {
|
|
const diff = abs(a.rot[i][j] - b.rot[i][j]);
|
|
if (diff > tolerance) {
|
|
throw `ERROR(${caller}): matrix[${i}][${j}] = ${a.rot[i][j]}, expected ${b.rot[i][j]}, diff ${diff}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function CompareVectors(caller, a, b, tolerance) {
|
|
let diff;
|
|
|
|
diff = abs(a.x - b.x);
|
|
if (diff > tolerance) {
|
|
throw `ERROR(${caller}): vector x = ${a.x}, expected ${b.x}, diff ${diff}`;
|
|
}
|
|
|
|
diff = abs(a.y - b.y);
|
|
if (diff > tolerance) {
|
|
throw `ERROR(${caller}): vector y = ${a.y}, expected ${b.y}, diff ${diff}`;
|
|
}
|
|
|
|
diff = abs(a.z - b.z);
|
|
if (diff > tolerance) {
|
|
throw `ERROR(${caller}): vector z = ${a.z}, expected ${b.z}, diff ${diff}`;
|
|
}
|
|
}
|
|
|
|
function Rotation_MatrixInverse() {
|
|
const a = Astronomy.MakeRotation([
|
|
[1, 4, 7],
|
|
[2, 5, 8],
|
|
[3, 6, 9]
|
|
]);
|
|
|
|
const v = Astronomy.MakeRotation([
|
|
[1, 2, 3],
|
|
[4, 5, 6],
|
|
[7, 8, 9]
|
|
]);
|
|
|
|
const b = Astronomy.InverseRotation(a);
|
|
CompareMatrices('Rotation_MatrixInverse', b, v, 0);
|
|
}
|
|
|
|
function Rotation_MatrixMultiply() {
|
|
const a = Astronomy.MakeRotation([
|
|
[1, 4, 7],
|
|
[2, 5, 8],
|
|
[3, 6, 9]
|
|
]);
|
|
|
|
const b = Astronomy.MakeRotation([
|
|
[10, 13, 16],
|
|
[11, 14, 17],
|
|
[12, 15, 18]
|
|
]);
|
|
|
|
const v = Astronomy.MakeRotation([
|
|
[84, 201, 318],
|
|
[90, 216, 342],
|
|
[96, 231, 366]
|
|
]);
|
|
|
|
const c = Astronomy.CombineRotation(b, a);
|
|
CompareMatrices('Rotation_MatrixMultiply', c, v, 0);
|
|
}
|
|
|
|
function VectorDiff(a, b) {
|
|
const dx = a.x - b.x;
|
|
const dy = a.y - b.y;
|
|
const dz = a.z - b.z;
|
|
return sqrt(dx*dx + dy*dy + dz*dz);
|
|
}
|
|
|
|
function Test_EQJ_ECL() {
|
|
const r = Astronomy.Rotation_EQJ_ECL();
|
|
|
|
/* Calculate heliocentric Earth position at a test time. */
|
|
const time = Astronomy.MakeTime(new Date('2019-12-08T19:39:15Z'));
|
|
const ev = Astronomy.HelioVector('Earth', time);
|
|
|
|
/* Use the existing Astronomy.Ecliptic() to calculate ecliptic vector and angles. */
|
|
const ecl = Astronomy.Ecliptic(ev);
|
|
if (Verbose) console.log(`JS Test_EQJ_ECL ecl = (${ecl.vec.x}, ${ecl.vec.y}, ${ecl.vec.z})`);
|
|
|
|
/* Now compute the same vector via rotation matrix. */
|
|
const ee = Astronomy.RotateVector(r, ev);
|
|
const dx = ee.x - ecl.vec.x;
|
|
const dy = ee.y - ecl.vec.y;
|
|
const dz = ee.z - ecl.vec.z;
|
|
const diff = sqrt(dx*dx + dy*dy + dz*dz);
|
|
if (Verbose) console.log(`JS Test_EQJ_ECL ee = (${ee.x}, ${ee.y}, ${ee.z}); diff = ${diff}`);
|
|
if (diff > 2.0e-15)
|
|
throw 'Test_EQJ_ECL: EXCESSIVE VECTOR ERROR';
|
|
|
|
/* Reverse the test: go from ecliptic back to equatorial. */
|
|
const ir = Astronomy.Rotation_ECL_EQJ();
|
|
const et = Astronomy.RotateVector(ir, ee);
|
|
const idiff = VectorDiff(et, ev);
|
|
if (Verbose) console.log(`JS Test_EQJ_ECL ev diff = ${idiff}`);
|
|
if (idiff > 2.0e-16)
|
|
throw 'Test_EQJ_ECL: EXCESSIVE REVERSE ROTATION ERROR';
|
|
}
|
|
|
|
function Test_EQJ_EQD(body) {
|
|
/* Verify conversion of equatorial J2000 to equatorial of-date, and back. */
|
|
/* Use established functions to calculate spherical coordinates for the body, in both EQJ and EQD. */
|
|
const time = Astronomy.MakeTime(new Date('2019-12-08T20:50:00Z'));
|
|
const observer = new Astronomy.Observer(+35, -85, 0);
|
|
const eq2000 = Astronomy.Equator(body, time, observer, false, true);
|
|
const eqdate = Astronomy.Equator(body, time, observer, true, true);
|
|
|
|
/* Convert EQJ spherical coordinates to vector. */
|
|
const v2000 = eq2000.vec;
|
|
|
|
/* Find rotation matrix. */
|
|
const r = Astronomy.Rotation_EQJ_EQD(time);
|
|
|
|
/* Rotate EQJ vector to EQD vector. */
|
|
const vdate = Astronomy.RotateVector(r, v2000);
|
|
|
|
/* Convert vector back to angular equatorial coordinates. */
|
|
let equcheck = Astronomy.EquatorFromVector(vdate);
|
|
|
|
/* Compare the result with the eqdate. */
|
|
const ra_diff = abs(equcheck.ra - eqdate.ra);
|
|
const dec_diff = abs(equcheck.dec - eqdate.dec);
|
|
const dist_diff = abs(equcheck.dist - eqdate.dist);
|
|
if (Verbose) console.log(`JS Test_EQJ_EQD: ${body} ra=${eqdate.ra}, dec=${eqdate.dec}, dist=${eqdate.dist}, ra_diff=${ra_diff}, dec_diff=${dec_diff}, dist_diff=${dist_diff}`);
|
|
if (ra_diff > 1.0e-14 || dec_diff > 1.0e-14 || dist_diff > 4.0e-15)
|
|
throw 'Test_EQJ_EQD: EXCESSIVE ERROR';
|
|
|
|
/* Perform the inverse conversion back to equatorial J2000 coordinates. */
|
|
const ir = Astronomy.Rotation_EQD_EQJ(time);
|
|
const t2000 = Astronomy.RotateVector(ir, vdate);
|
|
const diff = VectorDiff(t2000, v2000);
|
|
if (Verbose) console.log(`JS Test_EQJ_EQD: ${body} inverse diff = ${diff}`);
|
|
if (diff > 5.0e-15)
|
|
throw 'Test_EQJ_EQD: EXCESSIVE INVERSE ERROR';
|
|
}
|
|
|
|
function Test_EQD_HOR(body) {
|
|
/* Use existing functions to calculate horizontal coordinates of the body for the time+observer. */
|
|
const time = Astronomy.MakeTime(new Date('1970-12-13T05:15:00Z'));
|
|
const observer = new Astronomy.Observer(-37, +45, 0);
|
|
const eqd = Astronomy.Equator(body, time, observer, true, true);
|
|
if (Verbose) console.log(`JS Test_EQD_HOR ${body}: OFDATE ra=${eqd.ra}, dec=${eqd.dec}`);
|
|
const hor = Astronomy.Horizon(time, observer, eqd.ra, eqd.dec, 'normal');
|
|
|
|
/* Calculate the position of the body as an equatorial vector of date. */
|
|
const vec_eqd = eqd.vec;
|
|
|
|
/* Calculate rotation matrix to convert equatorial J2000 vector to horizontal vector. */
|
|
const rot = Astronomy.Rotation_EQD_HOR(time, observer);
|
|
|
|
/* Rotate the equator of date vector to a horizontal vector. */
|
|
const vec_hor = Astronomy.RotateVector(rot, vec_eqd);
|
|
|
|
/* Convert the horizontal vector to horizontal angular coordinates. */
|
|
const xsphere = Astronomy.HorizonFromVector(vec_hor, 'normal');
|
|
const diff_alt = abs(xsphere.lat - hor.altitude);
|
|
const diff_az = abs(xsphere.lon - hor.azimuth);
|
|
|
|
if (Verbose) console.log(`JS Test_EQD_HOR ${body}: trusted alt=${hor.altitude}, az=${hor.azimuth}; test alt=${xsphere.lat}, az=${xsphere.lon}; diff_alt=${diff_alt}, diff_az=${diff_az}`);
|
|
if (diff_alt > 4.0e-14 || diff_az > 1.0e-13)
|
|
throw 'Test_EQD_HOR: EXCESSIVE HORIZONTAL ERROR.';
|
|
|
|
/* Confirm that we can convert back to horizontal vector. */
|
|
const check_hor = Astronomy.VectorFromHorizon(xsphere, time, 'normal');
|
|
let diff = VectorDiff(check_hor, vec_hor);
|
|
if (Verbose) console.log(`JS Test_EQD_HOR ${body}: horizontal recovery: diff = ${diff}`);
|
|
if (diff > 2.0e-15)
|
|
throw 'Test_EQD_HOR: EXCESSIVE ERROR IN HORIZONTAL RECOVERY.';
|
|
|
|
/* Verify the inverse translation from horizontal vector to equatorial of-date vector. */
|
|
const irot = Astronomy.Rotation_HOR_EQD(time, observer);
|
|
const check_eqd = Astronomy.RotateVector(irot, vec_hor);
|
|
diff = VectorDiff(check_eqd, vec_eqd);
|
|
if (Verbose) console.log(`JS Test_EQD_HOR ${body}: OFDATE inverse rotation diff = ${diff}`);
|
|
if (diff > 2.0e-15)
|
|
throw 'Test_EQD_HOR: EXCESSIVE OFDATE INVERSE HORIZONTAL ERROR.';
|
|
|
|
/* Exercise HOR to EQJ translation. */
|
|
const eqj = Astronomy.Equator(body, time, observer, false, true);
|
|
const vec_eqj = eqj.vec;
|
|
const yrot = Astronomy.Rotation_HOR_EQJ(time, observer);
|
|
const check_eqj = Astronomy.RotateVector(yrot, vec_hor);
|
|
diff = VectorDiff(check_eqj, vec_eqj);
|
|
if (Verbose) console.log(`JS Test_EQD_HOR ${body}: J2000 inverse rotation diff = ${diff}`);
|
|
if (diff > 6.0e-15)
|
|
throw 'Test_EQD_HOR: EXCESSIVE J2000 INVERSE HORIZONTAL ERROR.';
|
|
|
|
/* Verify the inverse translation: EQJ to HOR. */
|
|
const zrot = Astronomy.Rotation_EQJ_HOR(time, observer);
|
|
const another_hor = Astronomy.RotateVector(zrot, vec_eqj);
|
|
diff = VectorDiff(another_hor, vec_hor);
|
|
if (Verbose) console.log(`JS Test_EQD_HOR ${body}: EQJ inverse rotation diff = ${diff}`);
|
|
if (diff > 3.0e-15)
|
|
throw 'Test_EQD_HOR: EXCESSIVE EQJ INVERSE HORIZONTAL ERROR.';
|
|
}
|
|
|
|
const IdentityMatrix = Astronomy.IdentityMatrix();
|
|
|
|
function CheckInverse(aname, bname, arot, brot) {
|
|
const crot = Astronomy.CombineRotation(arot, brot);
|
|
CompareMatrices(`CheckInverse(${aname},${bname})`, crot, IdentityMatrix, 2.0e-15);
|
|
}
|
|
|
|
function CheckCycle(cyclename, arot, brot, crot) {
|
|
const xrot = Astronomy.CombineRotation(arot, brot);
|
|
const irot = Astronomy.InverseRotation(xrot);
|
|
CompareMatrices(cyclename, crot, irot, 2.0e-15);
|
|
}
|
|
|
|
function Test_RotRoundTrip() {
|
|
const time = Astronomy.MakeTime(new Date('2067-05-30T14:45:00Z'));
|
|
const observer = new Astronomy.Observer(+28, -82, 0);
|
|
|
|
/*
|
|
In each round trip, calculate a forward rotation and a backward rotation.
|
|
Verify the two are inverse matrices.
|
|
*/
|
|
|
|
/* Round trip #1: EQJ <==> EQD. */
|
|
const eqj_eqd = Astronomy.Rotation_EQJ_EQD(time);
|
|
const eqd_eqj = Astronomy.Rotation_EQD_EQJ(time);
|
|
CheckInverse('eqj_eqd', 'eqd_eqj', eqj_eqd, eqd_eqj);
|
|
|
|
/* Round trip #2: EQJ <==> ECL. */
|
|
const eqj_ecl = Astronomy.Rotation_EQJ_ECL();
|
|
const ecl_eqj = Astronomy.Rotation_ECL_EQJ();
|
|
CheckInverse('eqj_ecl', 'ecl_eqj', eqj_ecl, ecl_eqj);
|
|
|
|
/* Round trip #3: EQJ <==> HOR. */
|
|
const eqj_hor = Astronomy.Rotation_EQJ_HOR(time, observer);
|
|
const hor_eqj = Astronomy.Rotation_HOR_EQJ(time, observer);
|
|
CheckInverse('eqj_hor', 'hor_eqj', eqj_hor, hor_eqj);
|
|
|
|
/* Round trip #4: EQD <==> HOR. */
|
|
const eqd_hor = Astronomy.Rotation_EQD_HOR(time, observer);
|
|
const hor_eqd = Astronomy.Rotation_HOR_EQD(time, observer);
|
|
CheckInverse('eqd_hor', 'hor_eqd', eqd_hor, hor_eqd);
|
|
|
|
/* Round trip #5: EQD <==> ECL. */
|
|
const eqd_ecl = Astronomy.Rotation_EQD_ECL(time);
|
|
const ecl_eqd = Astronomy.Rotation_ECL_EQD(time);
|
|
CheckInverse('eqd_ecl', 'ecl_eqd', eqd_ecl, ecl_eqd);
|
|
|
|
/* Round trip #6: HOR <==> ECL. */
|
|
const hor_ecl = Astronomy.Rotation_HOR_ECL(time, observer);
|
|
const ecl_hor = Astronomy.Rotation_ECL_HOR(time, observer);
|
|
CheckInverse('hor_ecl', 'ecl_hor', hor_ecl, ecl_hor);
|
|
|
|
/*
|
|
Verify that combining different sequences of rotations result
|
|
in the expected combination.
|
|
For example, (EQJ ==> HOR ==> ECL) must be the same matrix as (EQJ ==> ECL).
|
|
Each of these is a "triangle" of relationships between 3 orientations.
|
|
There are 4 possible ways to pick 3 orientations from the 4 to form a triangle.
|
|
Because we have just proved that each transformation is reversible,
|
|
we only need to verify the triangle in one cyclic direction.
|
|
*/
|
|
CheckCycle('eqj_ecl, ecl_eqd, eqd_eqj', eqj_ecl, ecl_eqd, eqd_eqj); /* excluded corner = HOR */
|
|
CheckCycle('eqj_hor, hor_ecl, ecl_eqj', eqj_hor, hor_ecl, ecl_eqj); /* excluded corner = EQD */
|
|
CheckCycle('eqj_hor, hor_eqd, eqd_eqj', eqj_hor, hor_eqd, eqd_eqj); /* excluded corner = ECL */
|
|
CheckCycle('ecl_eqd, eqd_hor, hor_ecl', ecl_eqd, eqd_hor, hor_ecl); /* excluded corner = EQJ */
|
|
|
|
if (Verbose) console.log('JS Test_RotRoundTrip: PASS');
|
|
}
|
|
|
|
function Rotation_Pivot() {
|
|
const tolerance = 1.0e-15;
|
|
|
|
/* Test #1 */
|
|
|
|
/* Start with an identity matrix. */
|
|
const ident = Astronomy.IdentityMatrix();
|
|
|
|
/* Pivot 90 degrees counterclockwise around the z-axis. */
|
|
let r = Astronomy.Pivot(ident, 2, +90.0);
|
|
|
|
/* Put the expected answer in 'a'. */
|
|
const a = Astronomy.MakeRotation([
|
|
[ 0, +1, 0],
|
|
[-1, 0, 0],
|
|
[ 0, 0, +1],
|
|
]);
|
|
|
|
/* Compare actual 'r' with expected 'a'. */
|
|
CompareMatrices('Rotation_Pivot #1', r, a, tolerance);
|
|
|
|
/* Test #2. */
|
|
|
|
/* Pivot again, -30 degrees around the x-axis. */
|
|
r = Astronomy.Pivot(r, 0, -30.0);
|
|
|
|
/* Pivot a third time, 180 degrees around the y-axis. */
|
|
r = Astronomy.Pivot(r, 1, +180.0);
|
|
|
|
/* Use the 'r' matrix to rotate a vector. */
|
|
const v1 = new Astronomy.Vector(1, 2, 3, Astronomy.MakeTime(0));
|
|
|
|
const v2 = Astronomy.RotateVector(r, v1);
|
|
|
|
/* Initialize the expected vector 've'. */
|
|
const ve = new Astronomy.Vector(+2.0, +2.3660254037844390, -2.0980762113533156, v1.t);
|
|
|
|
CompareVectors('Rotation_Pivot #2', v2, ve, tolerance);
|
|
|
|
if (Verbose) console.log('JS Rotation_Pivot: PASS');
|
|
}
|
|
|
|
|
|
Rotation_MatrixInverse();
|
|
Rotation_MatrixMultiply();
|
|
Rotation_Pivot();
|
|
Test_EQJ_ECL();
|
|
|
|
Test_EQJ_EQD('Mercury');
|
|
Test_EQJ_EQD('Venus');
|
|
Test_EQJ_EQD('Mars');
|
|
Test_EQJ_EQD('Jupiter');
|
|
Test_EQJ_EQD('Saturn');
|
|
|
|
Test_EQD_HOR('Mercury');
|
|
Test_EQD_HOR('Venus');
|
|
Test_EQD_HOR('Mars');
|
|
Test_EQD_HOR('Jupiter');
|
|
Test_EQD_HOR('Saturn');
|
|
|
|
Test_RotRoundTrip();
|
|
|
|
console.log('JS Rotation: PASS');
|
|
return 0;
|
|
}
|
|
|
|
|
|
function Refraction() {
|
|
for (let alt = -90.1; alt <= +90.1; alt += 0.001) {
|
|
const refr = Astronomy.Refraction('normal', alt);
|
|
const corrected = alt + refr;
|
|
const inv_refr = Astronomy.InverseRefraction('normal', corrected);
|
|
const check_alt = corrected + inv_refr;
|
|
const diff = abs(check_alt - alt);
|
|
if (diff > 2.0e-14)
|
|
throw `JS Refraction: alt=${alt}, refr=${refr}, diff=${diff}`;
|
|
}
|
|
|
|
console.log('JS Refraction: PASS');
|
|
return 0;
|
|
}
|
|
|
|
|
|
function Magnitude() {
|
|
function LoadMagnitudeData(filename) {
|
|
const text = fs.readFileSync(filename, 'utf8');
|
|
const lines = text.split(/[\r\n]+/);
|
|
let lnum = 0;
|
|
let rows = [];
|
|
for (let line of lines) {
|
|
++lnum;
|
|
|
|
// [ Date__(UT)__HR:MN APmag S-brt r rdot delta deldot S-T-O]
|
|
// [ 2023-Mar-30 00:00 -4.01 1.17 0.719092953368 -0.1186373 1.20453495004726 -11.0204917 55.9004]
|
|
let m = line.match(/^\s(\d{4})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2})\s(\d{2}):(\d{2})\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s*$/);
|
|
if (m) {
|
|
const year = int(m[1]);
|
|
const month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'].indexOf(m[2]);
|
|
const day = int(m[3]);
|
|
const hour = int(m[4]);
|
|
const minute = int(m[5]);
|
|
|
|
const item = {
|
|
lnum: lnum,
|
|
date: new Date(Date.UTC(year, month, day, hour, minute)),
|
|
mag: float(m[6]),
|
|
sbrt: float(m[7]),
|
|
helio_dist: float(m[8]),
|
|
helio_radvel: float(m[9]),
|
|
geo_dist: float(m[10]),
|
|
geo_radvel: float(m[11]),
|
|
phase_angle: float(m[12])
|
|
};
|
|
|
|
rows.push(item);
|
|
}
|
|
}
|
|
|
|
return {
|
|
filename: filename,
|
|
rows: rows
|
|
};
|
|
}
|
|
|
|
function CheckMagnitudeData(body, data) {
|
|
let diff_lo, diff_hi;
|
|
let sum_squared_diff = 0;
|
|
for (let item of data.rows) {
|
|
let illum = Astronomy.Illumination(body, item.date);
|
|
let diff = illum.mag - item.mag;
|
|
sum_squared_diff += diff*diff;
|
|
if (diff_lo === undefined) {
|
|
diff_lo = diff_hi = diff;
|
|
} else {
|
|
diff_lo = min(diff_lo, diff);
|
|
diff_hi = max(diff_hi, diff);
|
|
}
|
|
}
|
|
let rms = sqrt(sum_squared_diff / data.rows.length);
|
|
const limit = 0.012;
|
|
const pass = (abs(diff_lo) < limit && abs(diff_hi) < limit);
|
|
if (!pass || Verbose) console.log(`JS ${body.padEnd(8)} ${pass?" ":"FAIL"} diff_lo=${diff_lo.toFixed(4).padStart(8)}, diff_hi=${diff_hi.toFixed(4).padStart(8)}, rms=${rms.toFixed(4).padStart(8)}`);
|
|
return pass;
|
|
}
|
|
|
|
function TestSaturn() {
|
|
// JPL Horizons does not include Saturn's rings in its magnitude models.
|
|
// I still don't have authoritative test data for Saturn's magnitude.
|
|
// For now, I just test for consistency with Paul Schlyter's formulas at:
|
|
// http://www.stjarnhimlen.se/comp/ppcomp.html#15
|
|
|
|
let success = true;
|
|
|
|
const data = [
|
|
{ date: '1972-01-01T00:00Z', mag: -0.31904865, tilt: +24.50061220 },
|
|
{ date: '1980-01-01T00:00Z', mag: +0.85213663, tilt: -1.85761461 },
|
|
{ date: '2009-09-04T00:00Z', mag: +1.01626809, tilt: +0.08380716 },
|
|
{ date: '2017-06-15T00:00Z', mag: -0.12318790, tilt: -26.60871409 },
|
|
{ date: '2019-05-01T00:00Z', mag: +0.32954097, tilt: -23.53880802 },
|
|
{ date: '2025-09-25T00:00Z', mag: +0.51286575, tilt: +1.52327932 },
|
|
{ date: '2032-05-15T00:00Z', mag: -0.04652109, tilt: +26.95717765 }
|
|
];
|
|
|
|
for (let item of data) {
|
|
let illum = Astronomy.Illumination('Saturn', new Date(item.date));
|
|
if (Verbose) console.log(`JS Saturn: date=${illum.time.date.toISOString()} mag=${illum.mag.toFixed(8).padStart(12)} ring_tilt=${illum.ring_tilt.toFixed(8).padStart(12)}`);
|
|
const mag_diff = abs(illum.mag - item.mag);
|
|
if (mag_diff > 1.0e-4) {
|
|
console.log(`ERROR: Excessive magnitude error ${mag_diff}`);
|
|
success = false;
|
|
}
|
|
const tilt_diff = abs(illum.ring_tilt - item.tilt);
|
|
if (tilt_diff > 3.0e-5) {
|
|
console.log(`ERROR: Excessive ring tilt error ${tilt_diff}`);
|
|
success = false;
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
function TestMaxMag(filename, body) {
|
|
// Test that we can find maximum magnitude events for Venus within
|
|
// ranges found using JPL Horizons ephemeris data that has been
|
|
// pre-processed by magnitude/findmax.py.
|
|
|
|
const text = fs.readFileSync(filename, 'utf8');
|
|
const lines = text.trim().split(/[\r\n]+/);
|
|
let date = new Date(Date.UTC(2000, 0, 1));
|
|
let max_diff = 0;
|
|
for (let line of lines) {
|
|
let token = line.split(/\s+/);
|
|
let date1 = new Date(token[0]);
|
|
let date2 = new Date(token[1]);
|
|
let evt = Astronomy.SearchPeakMagnitude(body, date);
|
|
if (evt.time.date < date1 || evt.time.date > date2)
|
|
throw `Event time ${evt.time.toString()} is outside the range ${date1.toISOString()} .. ${date2.toISOString()}`;
|
|
|
|
// How close are we to the center date?
|
|
let date_center = new Date((date1.getTime() + date2.getTime())/2);
|
|
let diff_hours = abs(evt.time.date - date_center) / (1000 * 3600);
|
|
if (diff_hours > 7.1)
|
|
throw `Excessive diff_hours = ${diff_hours} from center date ${date_center.toISOString()}`;
|
|
|
|
max_diff = max(max_diff, diff_hours);
|
|
date = date2;
|
|
}
|
|
if (Verbose) console.log(`JS TestMaxMag: ${lines.length} events, max error = ${max_diff.toFixed(3)} hours.`);
|
|
return true;
|
|
}
|
|
|
|
let all_passed = true;
|
|
const bodies = ['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Uranus', 'Neptune', 'Pluto'];
|
|
for (let body of bodies) {
|
|
const data = LoadMagnitudeData(`magnitude/${body}.txt`);
|
|
if (!CheckMagnitudeData(body, data))
|
|
all_passed = false;
|
|
}
|
|
|
|
if (!TestSaturn())
|
|
all_passed = false;
|
|
|
|
if (!TestMaxMag('magnitude/maxmag_Venus.txt', 'Venus'))
|
|
all_passed = false;
|
|
|
|
all_passed || Fail('Found excessive error in at least one test.');
|
|
console.log('JS Magnitude: PASS');
|
|
return 0;
|
|
}
|
|
|
|
|
|
function Constellation() {
|
|
const filename = 'constellation/test_input.txt';
|
|
const text = fs.readFileSync(filename, {encoding:'utf8'});
|
|
const lines = text.trimRight().split('\n');
|
|
let lnum = 0;
|
|
let failcount = 0;
|
|
for (let row of lines) {
|
|
++lnum;
|
|
let token = row.trim().split(/\s+/);
|
|
if (token.length !== 4) {
|
|
Fail(`Bad data in ${filename} line ${lnum}: found ${token.length} tokens.`);
|
|
}
|
|
const id = int(token[0]);
|
|
const ra = float(token[1]);
|
|
const dec = float(token[2]);
|
|
const symbol = token[3];
|
|
if (!/^[A-Z][A-Za-z]{2}$/.test(symbol)) {
|
|
Fail(`Invalid symbol "${symbol}" in ${filename} line ${lnum}`);
|
|
}
|
|
const constel = Astronomy.Constellation(ra, dec);
|
|
if (constel.symbol !== symbol) {
|
|
++failcount;
|
|
console.error(`Star ${id}: expected ${symbol}, found ${constel.symbol} at B1875 RA=${constel.ra1875}, DEC=${constel.dec1875}`);
|
|
}
|
|
}
|
|
if (failcount > 0) {
|
|
Fail(`JS Constellation: ${failcount} failures.`);
|
|
}
|
|
console.log(`JS Constellation: PASS (verified ${lines.length})`);
|
|
return 0;
|
|
}
|
|
|
|
|
|
function TransitFile(body, filename, limit_minutes, limit_sep) {
|
|
const text = fs.readFileSync(filename, {encoding: 'utf8'});
|
|
const lines = text.trimRight().split('\n');
|
|
let lnum = 0;
|
|
let max_minutes = 0;
|
|
let max_sep = 0;
|
|
let transit = Astronomy.SearchTransit(body, Astronomy.MakeTime(new Date(Date.UTC(1600, 0))));
|
|
for (let row of lines) {
|
|
++lnum;
|
|
let token = row.trim().split(/\s+/);
|
|
|
|
/* 22:17 1881-11-08T00:57Z 03:38 3.8633 */
|
|
if (token.length !== 4) {
|
|
Fail(`JS TransitFile(${filename} line ${lnum}): bad data format.`);
|
|
}
|
|
const textp = token[1];
|
|
const text1 = textp.substr(0, 11) + token[0] + 'Z';
|
|
const text2 = textp.substr(0, 11) + token[2] + 'Z';
|
|
let timep = Astronomy.MakeTime(new Date(textp));
|
|
let time1 = Astronomy.MakeTime(new Date(text1));
|
|
let time2 = Astronomy.MakeTime(new Date(text2));
|
|
const separation = float(token[3]);
|
|
|
|
// If the start time is after the peak time, it really starts on the previous day.
|
|
if (time1.ut > timep.ut)
|
|
time1 = time1.AddDays(-1.0);
|
|
|
|
// If the finish time is before the peak time, it really starts on the following day.
|
|
if (time2.ut < timep.ut)
|
|
time2 = time2.AddDays(+1.0);
|
|
|
|
const diff_start = (24.0 * 60.0) * abs(time1.ut - transit.start.ut );
|
|
const diff_peak = (24.0 * 60.0) * abs(timep.ut - transit.peak.ut );
|
|
const diff_finish = (24.0 * 60.0) * abs(time2.ut - transit.finish.ut);
|
|
const diff_sep = abs(separation - transit.separation);
|
|
|
|
max_minutes = max(max_minutes, diff_start);
|
|
max_minutes = max(max_minutes, diff_peak);
|
|
max_minutes = max(max_minutes, diff_finish);
|
|
if (max_minutes > limit_minutes) {
|
|
Fail(`JS TransitFile(${filename} line ${lnum}): EXCESSIVE TIME ERROR = ${max_minutes} minutes.`);
|
|
}
|
|
|
|
max_sep = max(max_sep, diff_sep);
|
|
if (max_sep > limit_sep) {
|
|
Fail(`JS TransitFile(${filename} line ${lnum}): EXCESSIVE SEPARATION ERROR = ${max_sep} arcminutes.`);
|
|
}
|
|
|
|
transit = Astronomy.NextTransit(body, transit.finish);
|
|
}
|
|
|
|
console.log(`JS TransitFile(${filename}): PASS - verified ${lnum}, max minutes = ${max_minutes}, max sep arcmin = ${max_sep}`);
|
|
return 0;
|
|
}
|
|
|
|
|
|
function Transit() {
|
|
if (0 !== TransitFile('Mercury', 'eclipse/mercury.txt', 10.710, 0.2121)) {
|
|
return 1;
|
|
}
|
|
|
|
if (0 !== TransitFile('Venus', 'eclipse/venus.txt', 9.109, 0.6772)) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
function PlutoCheckDate(ut, arcmin_tolerance, x, y, z) {
|
|
const time = Astronomy.MakeTime(ut);
|
|
const timeText = time.toString();
|
|
if (Verbose) console.log(`JS PlutoCheck: ${timeText} = ${time.ut} UT = ${time.tt} TT`);
|
|
const vector = Astronomy.HelioVector('Pluto', time);
|
|
const dx = v(vector.x - x);
|
|
const dy = v(vector.y - y);
|
|
const dz = v(vector.z - z);
|
|
const diff = sqrt(dx*dx + dy*dy + dz*dz);
|
|
const dist = (sqrt(x*x + y*y + z*z) - 1.0); /* worst-case distance between Pluto and Earth */
|
|
const arcmin = (diff / dist) * (180.0 * 60.0 / Math.PI);
|
|
if (Verbose) console.log(`JS PlutoCheck: calc pos = [${vector.x}, ${vector.y}, ${vector.z}]`);
|
|
if (Verbose) console.log(`JS PlutoCheck: ref pos = [${x}, ${y}, ${z}]`);
|
|
if (Verbose) console.log(`JS PlutoCheck: del pos = [${vector.x - x}, ${vector.y - y}, ${vector.z - z}]`);
|
|
if (Verbose) console.log(`JS PlutoCheck: diff = ${diff} AU, ${arcmin} arcmin`);
|
|
if (Verbose) console.log("");
|
|
if (v(arcmin) > arcmin_tolerance) {
|
|
console.error("JS PlutoCheck: EXCESSIVE ERROR");
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
function PlutoCheck() {
|
|
if (0 != PlutoCheckDate( +18250.0, 0.271, +37.4377303523676090, -10.2466292454075898, -14.4773101310875809)) return 1;
|
|
if (0 != PlutoCheckDate( +18250.0, 0.271, +37.4377303523676090, -10.2466292454075898, -14.4773101310875809)) return 1;
|
|
if (0 != PlutoCheckDate( -856493.0, 6.636, +23.4292113199166252, +42.1452685817740829, +6.0580908436642940)) return 1;
|
|
if (0 != PlutoCheckDate( +435633.0, 0.058, -27.3178902095231813, +18.5887022581070305, +14.0493896259306936)) return 1;
|
|
if (0 != PlutoCheckDate( 0.0, 4.0e-9, -9.8753673425269000, -27.9789270580402771, -5.7537127596369588)) return 1;
|
|
if (0 != PlutoCheckDate( +800916.0, 6.705, -29.5266052645301365, +12.0554287322176474, +12.6878484911631091)) return 1;
|
|
console.log("JS PlutoCheck: PASS");
|
|
return 0;
|
|
}
|
|
|
|
|
|
function GeoidTestCase(time, observer, ofdate) {
|
|
let topo_moon = Astronomy.Equator(Astronomy.Body.Moon, time, observer, ofdate, false);
|
|
let surface = Astronomy.ObserverVector(time, observer, ofdate);
|
|
let geo_moon = Astronomy.GeoVector(Astronomy.Body.Moon, time, false);
|
|
|
|
if (ofdate) {
|
|
// GeoVector() returns J2000 coordinates. Convert to equator-of-date coordinates.
|
|
const rot = Astronomy.Rotation_EQJ_EQD(time);
|
|
geo_moon = Astronomy.RotateVector(rot, geo_moon);
|
|
}
|
|
|
|
const dx = Astronomy.KM_PER_AU * v((geo_moon.x - surface.x) - topo_moon.vec.x);
|
|
const dy = Astronomy.KM_PER_AU * v((geo_moon.y - surface.y) - topo_moon.vec.y);
|
|
const dz = Astronomy.KM_PER_AU * v((geo_moon.z - surface.z) - topo_moon.vec.z);
|
|
const diff = sqrt(dx*dx + dy*dy + dz*dz);
|
|
if (Verbose) console.log(`JS GeoidTestCase: ofdate=${ofdate}, time=${time.toISOString()}, lat=${observer.latitude}, lon=${observer.longitude}, ht=${observer.height}, surface=(${Astronomy.KM_PER_AU * surface.x}, ${Astronomy.KM_PER_AU * surface.y}, ${Astronomy.KM_PER_AU * surface.z}), diff = ${diff} km`);
|
|
|
|
// Require 1 millimeter accuracy! (one millionth of a kilometer).
|
|
if (diff > 1.0e-6) {
|
|
console.error('JS GeoidTestCase: EXCESSIVE POSITION ERROR.');
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
function Geoid() {
|
|
const time_list = [
|
|
new Date('1066-09-27T18:00:00Z'),
|
|
new Date('1970-12-13T15:42:00Z'),
|
|
new Date('1970-12-13T15:43:00Z'),
|
|
new Date('2015-03-05T02:15:45Z')
|
|
];
|
|
|
|
const observer_list = [
|
|
new Astronomy.Observer( +1.5, +2.7, 7.4),
|
|
new Astronomy.Observer(-53.7, +141.7, +100.0),
|
|
new Astronomy.Observer(+30.0, -85.2, -50.0),
|
|
new Astronomy.Observer(+90.0, +45.0, -50.0),
|
|
new Astronomy.Observer(-90.0, -180.0, 0.0)
|
|
];
|
|
|
|
// Test a variety of times and locations, in both supported orientation systems.
|
|
|
|
for (let observer of observer_list) {
|
|
for (let time of time_list) {
|
|
if (0 != GeoidTestCase(time, observer, false)) return 1;
|
|
if (0 != GeoidTestCase(time, observer, true)) return 1;
|
|
}
|
|
}
|
|
|
|
console.log('JS GeoidTest: PASS');
|
|
return 0;
|
|
}
|
|
|
|
|
|
const UnitTests = {
|
|
constellation: Constellation,
|
|
elongation: Elongation,
|
|
geoid: Geoid,
|
|
global_solar_eclipse: GlobalSolarEclipse,
|
|
local_solar_eclipse: LocalSolarEclipse,
|
|
lunar_apsis: LunarApsis,
|
|
lunar_eclipse: LunarEclipse,
|
|
lunar_eclipse_78: LunarEclipseIssue78,
|
|
planet_apsis: PlanetApsis,
|
|
pluto: PlutoCheck,
|
|
magnitude: Magnitude,
|
|
moon_phase: MoonPhase,
|
|
refraction: Refraction,
|
|
rise_set: RiseSet,
|
|
rotation: Rotation,
|
|
seasons: Seasons,
|
|
transit: Transit,
|
|
};
|
|
|
|
|
|
function TestAll() {
|
|
for (let name in UnitTests) {
|
|
if (UnitTests[name]()) {
|
|
return 1;
|
|
}
|
|
}
|
|
console.log('JS ALL PASS');
|
|
}
|
|
|
|
|
|
function main() {
|
|
let args = process.argv.slice(2);
|
|
if (args.length > 0 && args[0] === '-v') {
|
|
Verbose = true;
|
|
args = args.slice(1);
|
|
}
|
|
|
|
if (args.length === 1) {
|
|
const name = args[0];
|
|
if (name === 'all') {
|
|
return TestAll();
|
|
}
|
|
if (name === 'astro_check') {
|
|
// This is a special case because the output is redirected and parsed.
|
|
// It needs to be invoked separately, and cannot emit any extraneous output.
|
|
return AstroCheck();
|
|
}
|
|
const func = UnitTests[name];
|
|
if (typeof func !== 'function') {
|
|
console.log(`test.js: Unknown unit test "${name}"`);
|
|
return 1;
|
|
}
|
|
return func();
|
|
}
|
|
|
|
console.log('test.js: Invalid command line arguments.');
|
|
return 1;
|
|
}
|
|
|
|
|
|
process.exit(main());
|