caelus 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/accuracy.json +36 -4
- package/dist/src/chart.d.ts +14 -0
- package/dist/src/chart.js +48 -10
- package/dist/src/core.d.ts +2 -0
- package/dist/src/eclipses.d.ts +24 -0
- package/dist/src/eclipses.js +163 -0
- package/dist/src/events.d.ts +7 -0
- package/dist/src/events.js +29 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/node-loader.js +3 -0
- package/dist/src/stars.d.ts +29 -0
- package/dist/src/stars.js +53 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ ephemeris files. 1:1 port of the Python reference, checked by golden fixtures.
|
|
|
10
10
|
true node ≤ 1′ vs SE's built-in ephemeris (≤ 1″ vs JPL DE431)
|
|
11
11
|
(vs full DE431 files, 1850–2149), angles and Placidus cusps ≤ 3.2″ — all
|
|
12
12
|
invisible at the arcminute display precision chart software uses.
|
|
13
|
-
2. TypeScript port verified against Python golden fixtures: **3,
|
|
13
|
+
2. TypeScript port verified against Python golden fixtures: **3,218 checks,
|
|
14
14
|
0 failures, worst deviation 1.64 nano-arcseconds.** The two implementations
|
|
15
15
|
are numerically identical.
|
|
16
16
|
|
package/accuracy.json
CHANGED
|
@@ -176,6 +176,30 @@
|
|
|
176
176
|
"max": "2.3",
|
|
177
177
|
"rms": "—",
|
|
178
178
|
"note": "Hamburg-school constant-element Kepler orbits, elements fitted to SE 2.10's built-in definitions (fit_uranian.py prints per-body figures; Zeus is fit-noise-limited at ~3″ heliocentric). Uranian practice works in arcminutes"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "Fixed stars (318-star catalog)",
|
|
182
|
+
"max": "0.6",
|
|
183
|
+
"rms": "—",
|
|
184
|
+
"note": "HYG-derived catalog (ICRS J2000 + proper motions, full 3D space motion); vs swe_fixstar fed the same rows. Floor is the IAU 1976 vs Vondrák precession difference"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "Star-anchored ayanamsas",
|
|
188
|
+
"max": "0.2",
|
|
189
|
+
"rms": "—",
|
|
190
|
+
"note": "galcent_0sag and true_citra computed from the apparent star (Galactic Center / Spica); sidereal Sun vs SE ≤0.19″"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"name": "Gauquelin sectors",
|
|
194
|
+
"max": "0.0001 sectors",
|
|
195
|
+
"rms": "—",
|
|
196
|
+
"note": "rise/set of disc center with refraction (SE method 3): exact to the rise/set bound"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"name": "Eclipses (solar + lunar)",
|
|
200
|
+
"max": "9 s",
|
|
201
|
+
"rms": "—",
|
|
202
|
+
"note": "times of maximum vs swe; types exact over 1990-2030 (92 lunar + 89 solar, zero mismatches); lunar magnitudes ≤0.0013 (Danjon parallax enlargement, recovered empirically). Contact times typically ≤15 s, minutes for grazing geometries. Global circumstances only — no ground paths"
|
|
179
203
|
}
|
|
180
204
|
],
|
|
181
205
|
"summary": [
|
|
@@ -211,10 +235,6 @@
|
|
|
211
235
|
"label": "Mean Lilith",
|
|
212
236
|
"bound": "≤ 1.3″"
|
|
213
237
|
},
|
|
214
|
-
{
|
|
215
|
-
"label": "Sidereal (5 ayanamsas)",
|
|
216
|
-
"bound": "≤ 0.3″ added"
|
|
217
|
-
},
|
|
218
238
|
{
|
|
219
239
|
"label": "8 new house systems",
|
|
220
240
|
"bound": "exact (0.0″)"
|
|
@@ -234,6 +254,18 @@
|
|
|
234
254
|
{
|
|
235
255
|
"label": "Uranian bodies",
|
|
236
256
|
"bound": "≤ 2.3″"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"label": "Fixed stars",
|
|
260
|
+
"bound": "≤ 0.6″"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"label": "Sidereal (7 ayanamsas)",
|
|
264
|
+
"bound": "≤ 0.3″ added"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"label": "Eclipses",
|
|
268
|
+
"bound": "types exact; max ≤ 9 s"
|
|
237
269
|
}
|
|
238
270
|
],
|
|
239
271
|
"v03_harness": "python/validate_swiss.py regenerates every figure above the line against pyswisseph 2.10 (Moshier mode)",
|
package/dist/src/chart.d.ts
CHANGED
|
@@ -78,6 +78,20 @@ export declare class Engine {
|
|
|
78
78
|
* Building block for the events module; chart consumers want
|
|
79
79
|
* position() instead. */
|
|
80
80
|
ecliptic(body: BodyId, jde: number): [number, number, number | null];
|
|
81
|
+
/** Degrees to subtract from a true-equinox tropical longitude. */
|
|
82
|
+
private ayanShift;
|
|
83
|
+
/** Apparent place of a catalog star: lon/lat/ra/dec (deg), sign, mag. */
|
|
84
|
+
fixedStar(name: string, jdUt: number, opts?: CalcOptions): {
|
|
85
|
+
lon: number;
|
|
86
|
+
lat: number;
|
|
87
|
+
ra: number;
|
|
88
|
+
dec: number;
|
|
89
|
+
mag: number;
|
|
90
|
+
sign: string;
|
|
91
|
+
signDeg: number;
|
|
92
|
+
};
|
|
93
|
+
/** Names in the loaded fixed-star catalog (sorted). */
|
|
94
|
+
starNames(): string[];
|
|
81
95
|
private lonOnly;
|
|
82
96
|
/** Apparent geocentric ecliptic longitude (deg). Tropical: true equinox
|
|
83
97
|
* of date. Sidereal: mean equinox minus ayanamsa. */
|
package/dist/src/chart.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** astroengine chart -- public API: natal charts, aspects, retrogrades. */
|
|
2
2
|
import { DEG, mod, jdTT, julianDay, ChebSeries, planetApparent, sunApparent, moonApparentSeries, moonApparentPrecise, plutoApparent, chironApparent, meanNode, trueNodeSeries, trueNodePrecise, equatorial, ayanamsa, AYANAMSA_J2000, meanLilith, topocentricEcl, oscApogeePrecise, oscApogeeSeries, KeplerOrbit, trueObliquity, nutation, plutoHeliocentric, vsopHeliocentric, precessEcliptic, J2000, } from "./core.js";
|
|
3
|
+
import { starApparent } from "./stars.js";
|
|
3
4
|
import * as H from "./houses.js";
|
|
4
5
|
const TWO_PI = 2 * Math.PI;
|
|
5
6
|
export const BODIES = [
|
|
@@ -28,11 +29,18 @@ function parseZodiac(zodiac) {
|
|
|
28
29
|
return null;
|
|
29
30
|
if (zodiac.startsWith("sidereal:")) {
|
|
30
31
|
const mode = zodiac.slice("sidereal:".length);
|
|
31
|
-
if (AYANAMSA_J2000[mode] !== undefined)
|
|
32
|
+
if (AYANAMSA_J2000[mode] !== undefined || STAR_AYANAMSAS[mode])
|
|
32
33
|
return mode;
|
|
33
34
|
}
|
|
34
35
|
throw new Error(`unknown zodiac ${JSON.stringify(zodiac)}`);
|
|
35
36
|
}
|
|
37
|
+
/** Star-anchored ayanamsas: the named star sits at the fixed sidereal
|
|
38
|
+
* longitude by definition (Galactic Center at 0 Sagittarius; Spica at
|
|
39
|
+
* 0 Libra "citra"). Need the fixed-star catalog loaded. */
|
|
40
|
+
const STAR_AYANAMSAS = {
|
|
41
|
+
galcent_0sag: ["Galactic Center", 240.0],
|
|
42
|
+
true_citra: ["Spica", 180.0],
|
|
43
|
+
};
|
|
36
44
|
const VSOP_BODIES = new Set([
|
|
37
45
|
"mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune",
|
|
38
46
|
]);
|
|
@@ -120,6 +128,39 @@ export class Engine {
|
|
|
120
128
|
return planetApparent(this.data, body, jde);
|
|
121
129
|
throw new Error(`no data loaded for body '${body}'`);
|
|
122
130
|
}
|
|
131
|
+
/** Degrees to subtract from a true-equinox tropical longitude. */
|
|
132
|
+
ayanShift(jde, mode) {
|
|
133
|
+
const star = STAR_AYANAMSAS[mode];
|
|
134
|
+
if (star) {
|
|
135
|
+
const s = this.data.fixedStars?.stars[star[0]];
|
|
136
|
+
if (!s)
|
|
137
|
+
throw new Error(`zodiac 'sidereal:${mode}' needs the fixed-star catalog loaded`);
|
|
138
|
+
const [lon] = starApparent(this.data, s, jde);
|
|
139
|
+
return mod(lon / DEG - star[1], 360);
|
|
140
|
+
}
|
|
141
|
+
return mod(nutation(this.data, jde)[0] / DEG + ayanamsa(jde, mode), 360);
|
|
142
|
+
}
|
|
143
|
+
/** Apparent place of a catalog star: lon/lat/ra/dec (deg), sign, mag. */
|
|
144
|
+
fixedStar(name, jdUt, opts = {}) {
|
|
145
|
+
const s = this.data.fixedStars?.stars[name];
|
|
146
|
+
if (!s)
|
|
147
|
+
throw new Error(`no fixed-star catalog entry for '${name}'`);
|
|
148
|
+
const mode = parseZodiac(opts.zodiac ?? "tropical");
|
|
149
|
+
const jde = jdTT(jdUt);
|
|
150
|
+
const [lonR, latR] = starApparent(this.data, s, jde);
|
|
151
|
+
const [ra, dec] = equatorial(lonR, latR, trueObliquity(this.data, jde));
|
|
152
|
+
let lon = lonR / DEG;
|
|
153
|
+
if (mode !== null)
|
|
154
|
+
lon = mod(lon - this.ayanShift(jde, mode), 360);
|
|
155
|
+
return {
|
|
156
|
+
lon, lat: latR / DEG, ra: ra / DEG, dec: dec / DEG, mag: s.mag,
|
|
157
|
+
sign: SIGNS[Math.floor(lon / 30)], signDeg: mod(lon, 30),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** Names in the loaded fixed-star catalog (sorted). */
|
|
161
|
+
starNames() {
|
|
162
|
+
return Object.keys(this.data.fixedStars?.stars ?? {}).sort();
|
|
163
|
+
}
|
|
123
164
|
lonOnly(body, jdUt, mode, topo) {
|
|
124
165
|
const jde = jdTT(jdUt);
|
|
125
166
|
let [lon, lat, dist] = this.ecliptic(body, jde);
|
|
@@ -128,9 +169,8 @@ export class Engine {
|
|
|
128
169
|
[lon, lat, dist] = topocentricEcl(lon, lat, dist, lst, topo.lat * DEG, topo.altM ?? 0.0, trueObliquity(this.data, jde));
|
|
129
170
|
}
|
|
130
171
|
let lonDeg = lon / DEG;
|
|
131
|
-
if (mode !== null)
|
|
132
|
-
lonDeg = mod(lonDeg -
|
|
133
|
-
}
|
|
172
|
+
if (mode !== null)
|
|
173
|
+
lonDeg = mod(lonDeg - this.ayanShift(jde, mode), 360);
|
|
134
174
|
return lonDeg;
|
|
135
175
|
}
|
|
136
176
|
/** Apparent geocentric ecliptic longitude (deg). Tropical: true equinox
|
|
@@ -186,9 +226,8 @@ export class Engine {
|
|
|
186
226
|
}
|
|
187
227
|
const [ra, dec] = equatorial(lonR, latR, trueObliquity(this.data, jde));
|
|
188
228
|
let lon = lonR / DEG;
|
|
189
|
-
if (mode !== null)
|
|
190
|
-
lon = mod(lon -
|
|
191
|
-
}
|
|
229
|
+
if (mode !== null)
|
|
230
|
+
lon = mod(lon - this.ayanShift(jde, mode), 360);
|
|
192
231
|
const h = 0.25; // days; central difference
|
|
193
232
|
const l0 = this.lonOnly(body, jdUt - h, mode, topo);
|
|
194
233
|
const l1 = this.lonOnly(body, jdUt + h, mode, topo);
|
|
@@ -276,9 +315,8 @@ export class Engine {
|
|
|
276
315
|
}
|
|
277
316
|
const jde = jdTT(jdUt);
|
|
278
317
|
let shift = 0.0;
|
|
279
|
-
if (mode !== null)
|
|
280
|
-
shift =
|
|
281
|
-
}
|
|
318
|
+
if (mode !== null)
|
|
319
|
+
shift = this.ayanShift(jde, mode);
|
|
282
320
|
const outDeg = (rad) => mod(rad / DEG - shift, 360);
|
|
283
321
|
let cuspsDeg;
|
|
284
322
|
if (mode !== null && used === "whole_sign") {
|
package/dist/src/core.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface EngineData {
|
|
|
56
56
|
chebPacks?: Record<string, ChebData>;
|
|
57
57
|
/** Hamburg-school (Uranian) constant-element orbits; see fit_uranian.py. */
|
|
58
58
|
keplerPack?: KeplerPack;
|
|
59
|
+
/** Fixed-star catalog (HYG-derived; ICRS J2000 + proper motions). */
|
|
60
|
+
fixedStars?: import("./stars.js").StarPack;
|
|
59
61
|
}
|
|
60
62
|
export declare function julianDay(y: number, mo: number, d: number, h?: number, mi?: number, s?: number): number;
|
|
61
63
|
/** TT - UT1 in seconds. Observed IERS 1955-2025, E&M polynomials before,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Engine } from "./chart.js";
|
|
2
|
+
export interface LunarEclipse {
|
|
3
|
+
tMax: number;
|
|
4
|
+
type: "total" | "partial" | "penumbral";
|
|
5
|
+
magUmbral: number;
|
|
6
|
+
magPenumbral: number;
|
|
7
|
+
penumbralBegin: number | null;
|
|
8
|
+
penumbralEnd: number | null;
|
|
9
|
+
partialBegin: number | null;
|
|
10
|
+
partialEnd: number | null;
|
|
11
|
+
totalBegin: number | null;
|
|
12
|
+
totalEnd: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface SolarEclipse {
|
|
15
|
+
tMax: number;
|
|
16
|
+
type: "total" | "annular" | "hybrid" | "partial";
|
|
17
|
+
gamma: number;
|
|
18
|
+
begin: number;
|
|
19
|
+
end: number;
|
|
20
|
+
}
|
|
21
|
+
/** Lunar eclipses in [jdStart, jdEnd] (UT JDs). */
|
|
22
|
+
export declare function lunarEclipses(engine: Engine, jdStart: number, jdEnd: number): LunarEclipse[];
|
|
23
|
+
/** Solar eclipses (global circumstances) in [jdStart, jdEnd] (UT JDs). */
|
|
24
|
+
export declare function solarEclipses(engine: Engine, jdStart: number, jdEnd: number): SolarEclipse[];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine eclipses -- solar and lunar eclipse search.
|
|
3
|
+
*
|
|
4
|
+
* Lunar: direct shadow geometry at the anti-solar point with Danjon's
|
|
5
|
+
* enlargement (lunar parallax x 86/85 on the flattened Earth) — the rule
|
|
6
|
+
* Swiss Ephemeris uses, recovered empirically: magnitudes match to 0.001,
|
|
7
|
+
* types exactly; times of maximum to ~9 s (contact times typically <=15 s,
|
|
8
|
+
* up to ~2 min for grazing geometries where the crossing flattens).
|
|
9
|
+
*
|
|
10
|
+
* Solar (global): shadow-axis geometry. gamma = closest approach of the
|
|
11
|
+
* Sun-Moon axis to the geocenter in Earth radii; the umbral cone's reach
|
|
12
|
+
* at the surface separates total from annular, a sign change along the
|
|
13
|
+
* track marks hybrids. Types match Swiss Ephemeris exactly over decades.
|
|
14
|
+
* Local circumstances (where/visibility) are not computed here.
|
|
15
|
+
*/
|
|
16
|
+
import { ARCSEC, jdTT, mod } from "./core.js";
|
|
17
|
+
const KM_PER_AU = 149597870.7;
|
|
18
|
+
const R_EARTH = 6378.14;
|
|
19
|
+
const R_SUN = 696000.0;
|
|
20
|
+
const R_MOON = 1737.4;
|
|
21
|
+
const PI_SUN = 8.794 * ARCSEC;
|
|
22
|
+
const DANJON = (1 + 1 / 85.0) * 0.99834;
|
|
23
|
+
function lunarGeom(engine, jd) {
|
|
24
|
+
const jde = jdTT(jd);
|
|
25
|
+
const [slon, slat, sdist] = engine.ecliptic("sun", jde);
|
|
26
|
+
const [mlon, mlat, mdist] = engine.ecliptic("moon", jde);
|
|
27
|
+
const alon = mod(slon + Math.PI, 2 * Math.PI);
|
|
28
|
+
const alat = -slat;
|
|
29
|
+
const cosd = Math.sin(alat) * Math.sin(mlat)
|
|
30
|
+
+ Math.cos(alat) * Math.cos(mlat) * Math.cos(alon - mlon);
|
|
31
|
+
const theta = Math.acos(Math.max(-1, Math.min(1, cosd)));
|
|
32
|
+
const mkm = mdist * KM_PER_AU;
|
|
33
|
+
const piEff = DANJON * Math.asin(R_EARTH / mkm);
|
|
34
|
+
const sM = Math.asin(R_MOON / mkm);
|
|
35
|
+
const sS = Math.asin(R_SUN / (sdist * KM_PER_AU));
|
|
36
|
+
return [theta, piEff - sS + PI_SUN, piEff + sS + PI_SUN, sM];
|
|
37
|
+
}
|
|
38
|
+
function solarGeom(engine, jd) {
|
|
39
|
+
const jde = jdTT(jd);
|
|
40
|
+
const [slon, slat, sdist] = engine.ecliptic("sun", jde);
|
|
41
|
+
const [mlon, mlat, mdist] = engine.ecliptic("moon", jde);
|
|
42
|
+
const vec = (lon, lat, r) => [
|
|
43
|
+
r * Math.cos(lat) * Math.cos(lon),
|
|
44
|
+
r * Math.cos(lat) * Math.sin(lon),
|
|
45
|
+
r * Math.sin(lat),
|
|
46
|
+
];
|
|
47
|
+
const S = vec(slon, slat, sdist * KM_PER_AU);
|
|
48
|
+
const M = vec(mlon, mlat, mdist * KM_PER_AU);
|
|
49
|
+
const SM = [M[0] - S[0], M[1] - S[1], M[2] - S[2]];
|
|
50
|
+
const smn = Math.sqrt(SM[0] ** 2 + SM[1] ** 2 + SM[2] ** 2);
|
|
51
|
+
const d = SM.map((c) => c / smn);
|
|
52
|
+
const t0 = -(M[0] * d[0] + M[1] * d[1] + M[2] * d[2]);
|
|
53
|
+
const P = [M[0] + t0 * d[0], M[1] + t0 * d[1], M[2] + t0 * d[2]];
|
|
54
|
+
const dAxis = Math.sqrt(P[0] ** 2 + P[1] ** 2 + P[2] ** 2);
|
|
55
|
+
const f1 = Math.asin((R_SUN + R_MOON) / smn);
|
|
56
|
+
const f2 = Math.asin((R_SUN - R_MOON) / smn);
|
|
57
|
+
const rPen = (R_MOON / Math.tan(f1) + t0) * Math.tan(f1);
|
|
58
|
+
const rUmb = (R_MOON / Math.tan(f2) - t0) * Math.tan(f2);
|
|
59
|
+
return [dAxis, rPen, rUmb, t0, f2];
|
|
60
|
+
}
|
|
61
|
+
function minimize(f, lo, hi) {
|
|
62
|
+
for (let i = 0; i < 60; i++) {
|
|
63
|
+
const m1 = lo + (hi - lo) / 3;
|
|
64
|
+
const m2 = hi - (hi - lo) / 3;
|
|
65
|
+
if (f(m1) < f(m2))
|
|
66
|
+
hi = m2;
|
|
67
|
+
else
|
|
68
|
+
lo = m1;
|
|
69
|
+
}
|
|
70
|
+
return (lo + hi) / 2;
|
|
71
|
+
}
|
|
72
|
+
function bisect(f, a, b) {
|
|
73
|
+
let fa = f(a);
|
|
74
|
+
for (let i = 0; i < 50; i++) {
|
|
75
|
+
const m = (a + b) / 2;
|
|
76
|
+
if (fa * f(m) <= 0) {
|
|
77
|
+
b = m;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
a = m;
|
|
81
|
+
fa = f(a);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return (a + b) / 2;
|
|
85
|
+
}
|
|
86
|
+
function syzygies(engine, jdStart, jdEnd, angle) {
|
|
87
|
+
const f = (t) => {
|
|
88
|
+
const e = mod(engine.longitude("moon", t) - engine.longitude("sun", t), 360);
|
|
89
|
+
return mod(e - angle + 180, 360) - 180;
|
|
90
|
+
};
|
|
91
|
+
const out = [];
|
|
92
|
+
const step = 5.0;
|
|
93
|
+
let prev = f(jdStart);
|
|
94
|
+
for (let t = jdStart + step; t <= jdEnd + step; t += step) {
|
|
95
|
+
const cur = f(t);
|
|
96
|
+
if (prev * cur < 0 && Math.abs(cur - prev) < 180) {
|
|
97
|
+
out.push(bisect(f, t - step, t));
|
|
98
|
+
}
|
|
99
|
+
prev = cur;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Lunar eclipses in [jdStart, jdEnd] (UT JDs). */
|
|
104
|
+
export function lunarEclipses(engine, jdStart, jdEnd) {
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const tFull of syzygies(engine, jdStart - 1, jdEnd + 1, 180.0)) {
|
|
107
|
+
const tMax = minimize((t) => lunarGeom(engine, t)[0], tFull - 0.3, tFull + 0.3);
|
|
108
|
+
const [theta, u, pen, sM] = lunarGeom(engine, tMax);
|
|
109
|
+
const magU = (u + sM - theta) / (2 * sM);
|
|
110
|
+
const magP = (pen + sM - theta) / (2 * sM);
|
|
111
|
+
if (magP <= 0 || tMax < jdStart || tMax > jdEnd)
|
|
112
|
+
continue;
|
|
113
|
+
const kind = magU >= 1 ? "total" : magU > 0 ? "partial" : "penumbral";
|
|
114
|
+
const cross = (idx, sign) => {
|
|
115
|
+
const f = (t) => {
|
|
116
|
+
const g = lunarGeom(engine, t);
|
|
117
|
+
return g[0] - (g[idx] + sign * g[3]);
|
|
118
|
+
};
|
|
119
|
+
return [bisect(f, tMax - 0.35, tMax), bisect(f, tMax, tMax + 0.35)];
|
|
120
|
+
};
|
|
121
|
+
const [penB, penE] = cross(2, 1);
|
|
122
|
+
const [parB, parE] = magU > 0 ? cross(1, 1) : [null, null];
|
|
123
|
+
const [totB, totE] = magU >= 1 ? cross(1, -1) : [null, null];
|
|
124
|
+
out.push({
|
|
125
|
+
tMax, type: kind,
|
|
126
|
+
magUmbral: Math.max(magU, 0), magPenumbral: magP,
|
|
127
|
+
penumbralBegin: penB, penumbralEnd: penE,
|
|
128
|
+
partialBegin: parB, partialEnd: parE,
|
|
129
|
+
totalBegin: totB, totalEnd: totE,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** Solar eclipses (global circumstances) in [jdStart, jdEnd] (UT JDs). */
|
|
135
|
+
export function solarEclipses(engine, jdStart, jdEnd) {
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const tNew of syzygies(engine, jdStart - 1, jdEnd + 1, 0.0)) {
|
|
138
|
+
const tMax = minimize((t) => solarGeom(engine, t)[0], tNew - 0.4, tNew + 0.4);
|
|
139
|
+
const [dAxis, rPen, rUmb, , f2] = solarGeom(engine, tMax);
|
|
140
|
+
if (dAxis > R_EARTH + rPen || tMax < jdStart || tMax > jdEnd)
|
|
141
|
+
continue;
|
|
142
|
+
const gamma = dAxis / R_EARTH;
|
|
143
|
+
let kind;
|
|
144
|
+
if (dAxis < R_EARTH) {
|
|
145
|
+
const depth = Math.sqrt(Math.max(R_EARTH ** 2 - dAxis ** 2, 0));
|
|
146
|
+
const rUmbSurface = rUmb + depth * Math.tan(f2);
|
|
147
|
+
kind = rUmb > 0 ? "total" : rUmbSurface > 0 ? "hybrid" : "annular";
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
kind = "partial";
|
|
151
|
+
}
|
|
152
|
+
const f = (t) => {
|
|
153
|
+
const g = solarGeom(engine, t);
|
|
154
|
+
return g[0] - (R_EARTH + g[1]);
|
|
155
|
+
};
|
|
156
|
+
out.push({
|
|
157
|
+
tMax, type: kind, gamma,
|
|
158
|
+
begin: bisect(f, tMax - 0.35, tMax),
|
|
159
|
+
end: bisect(f, tMax, tMax + 0.35),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
package/dist/src/events.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface RiseSetOptions {
|
|
|
5
5
|
pressure?: number;
|
|
6
6
|
tempC?: number;
|
|
7
7
|
searchDays?: number;
|
|
8
|
+
/** Rise/set of the disc center instead of the upper limb. */
|
|
9
|
+
discCenter?: boolean;
|
|
8
10
|
}
|
|
9
11
|
/** Next rise/set/meridian transit (UT JD) after jdStart, or null when the
|
|
10
12
|
* event does not occur in the window (polar day/night). */
|
|
@@ -20,3 +22,8 @@ export declare function lunarPhases(engine: Engine, jdStart: number, jdEnd: numb
|
|
|
20
22
|
* turns]. Sun and Moon never station. Station timing is ill-conditioned:
|
|
21
23
|
* expect minute-level differences between ephemerides. */
|
|
22
24
|
export declare function stations(engine: Engine, body: BodyId, jdStart: number, jdEnd: number, maxHits?: number): Array<[number, "retrograde" | "direct"]>;
|
|
25
|
+
/** Gauquelin sector (1..36, float) from rise/set times of the disc center
|
|
26
|
+
* with refraction (Swiss Ephemeris method 3). Sectors run from rise: 1-18
|
|
27
|
+
* above the horizon, 19-36 below. Null in polar no-rise/no-set
|
|
28
|
+
* conditions. */
|
|
29
|
+
export declare function gauquelinSector(engine: Engine, body: BodyId, jdUt: number, latDeg: number, lonDeg: number): number | null;
|
package/dist/src/events.js
CHANGED
|
@@ -73,7 +73,7 @@ export function riseSet(engine, body, jdStart, latDeg, lonDeg, kind = "rise", op
|
|
|
73
73
|
const [alt, , dist] = topoAltHa(engine, body, t, latDeg, lonDeg, altM);
|
|
74
74
|
let sd = 0.0;
|
|
75
75
|
const diam = DIAMETER_KM[body];
|
|
76
|
-
if (diam !== undefined && dist !== null) {
|
|
76
|
+
if (!opts.discCenter && diam !== undefined && dist !== null) {
|
|
77
77
|
sd = Math.asin(diam / (2 * dist * KM_PER_AU));
|
|
78
78
|
}
|
|
79
79
|
const h0 = -((R0_ARCMIN / 60.0) * scale * DEG + sd);
|
|
@@ -154,3 +154,31 @@ export function stations(engine, body, jdStart, jdEnd, maxHits = 30) {
|
|
|
154
154
|
}
|
|
155
155
|
return out;
|
|
156
156
|
}
|
|
157
|
+
/** Gauquelin sector (1..36, float) from rise/set times of the disc center
|
|
158
|
+
* with refraction (Swiss Ephemeris method 3). Sectors run from rise: 1-18
|
|
159
|
+
* above the horizon, 19-36 below. Null in polar no-rise/no-set
|
|
160
|
+
* conditions. */
|
|
161
|
+
export function gauquelinSector(engine, body, jdUt, latDeg, lonDeg) {
|
|
162
|
+
const surrounding = (kind) => {
|
|
163
|
+
let t = riseSet(engine, body, jdUt - 1.3, latDeg, lonDeg, kind, { discCenter: true });
|
|
164
|
+
let prev = null;
|
|
165
|
+
while (t !== null && t <= jdUt) {
|
|
166
|
+
prev = t;
|
|
167
|
+
t = riseSet(engine, body, t + 1e-4, latDeg, lonDeg, kind, { discCenter: true });
|
|
168
|
+
}
|
|
169
|
+
return [prev, t];
|
|
170
|
+
};
|
|
171
|
+
const [prevRise] = surrounding("rise");
|
|
172
|
+
const [prevSet, nextSetA] = surrounding("set");
|
|
173
|
+
if (prevRise === null || prevSet === null)
|
|
174
|
+
return null;
|
|
175
|
+
if (prevRise > prevSet) {
|
|
176
|
+
if (nextSetA === null)
|
|
177
|
+
return null;
|
|
178
|
+
return 1 + (18 * (jdUt - prevRise)) / (nextSetA - prevRise);
|
|
179
|
+
}
|
|
180
|
+
const [, nextRise] = surrounding("rise");
|
|
181
|
+
if (nextRise === null)
|
|
182
|
+
return null;
|
|
183
|
+
return 19 + (18 * (jdUt - prevSet)) / (nextRise - prevSet);
|
|
184
|
+
}
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
package/dist/src/node-loader.js
CHANGED
|
@@ -31,6 +31,9 @@ export function loadNodeData(dir, level = "embedded", moonTier = "full") {
|
|
|
31
31
|
if (existsSync(join(dir, "uranian_kepler.json"))) {
|
|
32
32
|
data.keplerPack = j("uranian_kepler.json");
|
|
33
33
|
}
|
|
34
|
+
if (existsSync(join(dir, "fixed_stars.json"))) {
|
|
35
|
+
data.fixedStars = j("fixed_stars.json");
|
|
36
|
+
}
|
|
34
37
|
// asteroid packs (Horizons fits): loaded when present, ~380 KB total
|
|
35
38
|
for (const b of ["ceres", "pallas", "juno", "vesta", "pholus"]) {
|
|
36
39
|
if (existsSync(join(dir, `${b}_cheb.json`))) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine stars -- fixed stars: apparent places from the HYG-derived
|
|
3
|
+
* catalog (data/fixed_stars.json; ICRS J2000 with proper motions).
|
|
4
|
+
*
|
|
5
|
+
* Chain: full 3D space motion (proper motion + radial velocity at the
|
|
6
|
+
* parallax distance) -> ICRS equatorial -> ecliptic J2000 -> IAU 1976
|
|
7
|
+
* precession to date -> annual aberration (classic elliptic form, as for
|
|
8
|
+
* Pluto/Chiron) -> nutation. Validated against swe_fixstar fed the same
|
|
9
|
+
* catalog rows: <=0.6 arcsec over 1900-2099 (the floor is the IAU 1976 vs
|
|
10
|
+
* Vondrak precession difference, shared with the rest of the engine).
|
|
11
|
+
*/
|
|
12
|
+
import { EngineData } from "./core.js";
|
|
13
|
+
export interface StarEntry {
|
|
14
|
+
ra: number;
|
|
15
|
+
dec: number;
|
|
16
|
+
pmra: number;
|
|
17
|
+
pmdec: number;
|
|
18
|
+
rv: number;
|
|
19
|
+
plx: number;
|
|
20
|
+
mag: number;
|
|
21
|
+
bayer: string;
|
|
22
|
+
}
|
|
23
|
+
export interface StarPack {
|
|
24
|
+
provenance: string;
|
|
25
|
+
frame: string;
|
|
26
|
+
stars: Record<string, StarEntry>;
|
|
27
|
+
}
|
|
28
|
+
/** Apparent ecliptic [lon, lat] of date (rad) for a catalog entry. */
|
|
29
|
+
export declare function starApparent(data: EngineData, s: StarEntry, jde: number): [number, number];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine stars -- fixed stars: apparent places from the HYG-derived
|
|
3
|
+
* catalog (data/fixed_stars.json; ICRS J2000 with proper motions).
|
|
4
|
+
*
|
|
5
|
+
* Chain: full 3D space motion (proper motion + radial velocity at the
|
|
6
|
+
* parallax distance) -> ICRS equatorial -> ecliptic J2000 -> IAU 1976
|
|
7
|
+
* precession to date -> annual aberration (classic elliptic form, as for
|
|
8
|
+
* Pluto/Chiron) -> nutation. Validated against swe_fixstar fed the same
|
|
9
|
+
* catalog rows: <=0.6 arcsec over 1900-2099 (the floor is the IAU 1976 vs
|
|
10
|
+
* Vondrak precession difference, shared with the rest of the engine).
|
|
11
|
+
*/
|
|
12
|
+
import { DEG, ARCSEC, J2000, mod, nutation, precessEcliptic, vsopHeliocentric, } from "./core.js";
|
|
13
|
+
const TWO_PI = 2 * Math.PI;
|
|
14
|
+
const KM_PER_AU = 149597870.7;
|
|
15
|
+
const AU_PER_PC = 206264.806;
|
|
16
|
+
/** Apparent ecliptic [lon, lat] of date (rad) for a catalog entry. */
|
|
17
|
+
export function starApparent(data, s, jde) {
|
|
18
|
+
const t = (jde - J2000) / 365.25;
|
|
19
|
+
const ra = s.ra * DEG;
|
|
20
|
+
const dec = s.dec * DEG;
|
|
21
|
+
const rAu = s.plx > 0 ? AU_PER_PC / (s.plx * 1e-3) : 1e9 * AU_PER_PC;
|
|
22
|
+
const cd = Math.cos(dec);
|
|
23
|
+
const sd = Math.sin(dec);
|
|
24
|
+
const cr = Math.cos(ra);
|
|
25
|
+
const sr = Math.sin(ra);
|
|
26
|
+
const p = [cd * cr, cd * sr, sd];
|
|
27
|
+
const east = [-sr, cr, 0.0];
|
|
28
|
+
const north = [-sd * cr, -sd * sr, cd];
|
|
29
|
+
const pmra = s.pmra * 1e-3 * ARCSEC;
|
|
30
|
+
const pmdec = s.pmdec * 1e-3 * ARCSEC;
|
|
31
|
+
const rv = (s.rv * 86400 * 365.25) / KM_PER_AU;
|
|
32
|
+
const pos = [0, 1, 2].map((i) => p[i] * rAu + (east[i] * pmra * rAu + north[i] * pmdec * rAu + p[i] * rv) * t);
|
|
33
|
+
const rn = Math.sqrt(pos[0] ** 2 + pos[1] ** 2 + pos[2] ** 2);
|
|
34
|
+
const x = pos[0] / rn;
|
|
35
|
+
const y = pos[1] / rn;
|
|
36
|
+
const z = pos[2] / rn;
|
|
37
|
+
const ra2 = Math.atan2(y, x);
|
|
38
|
+
const dec2 = Math.asin(z);
|
|
39
|
+
const e0 = 84381.448 * ARCSEC;
|
|
40
|
+
let lat = Math.asin(Math.sin(dec2) * Math.cos(e0) - Math.cos(dec2) * Math.sin(e0) * Math.sin(ra2));
|
|
41
|
+
let lon = mod(Math.atan2(Math.sin(ra2) * Math.cos(e0) + Math.tan(dec2) * Math.sin(e0), Math.cos(ra2)), TWO_PI);
|
|
42
|
+
[lon, lat] = precessEcliptic(lon, lat, J2000, jde);
|
|
43
|
+
const [L0] = vsopHeliocentric(data.vsop.earth, jde);
|
|
44
|
+
const sunLon = mod(L0 + Math.PI, TWO_PI);
|
|
45
|
+
const T = (jde - J2000) / 36525.0;
|
|
46
|
+
const k = 20.4898 * ARCSEC;
|
|
47
|
+
const e = 0.016708634 - 0.000042037 * T;
|
|
48
|
+
const piPer = (102.93735 + 1.71946 * T) * DEG;
|
|
49
|
+
lon += (-k * Math.cos(sunLon - lon) + e * k * Math.cos(piPer - lon)) / Math.cos(lat);
|
|
50
|
+
lat += -k * Math.sin(lat) * (Math.sin(sunLon - lon) - e * Math.sin(piPer - lon));
|
|
51
|
+
lon = mod(lon + nutation(data, jde)[0], TWO_PI);
|
|
52
|
+
return [lon, lat];
|
|
53
|
+
}
|