caelus 0.4.0 → 0.6.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 +2 -2
- package/accuracy.json +39 -7
- package/dist/src/chart.d.ts +14 -0
- package/dist/src/chart.js +48 -10
- package/dist/src/core.d.ts +7 -4
- package/dist/src/core.js +133 -43
- 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 +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/node-loader.js +3 -0
- package/dist/src/query.d.ts +32 -0
- package/dist/src/query.js +161 -0
- package/dist/src/stars.d.ts +28 -0
- package/dist/src/stars.js +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ 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,
|
|
14
|
-
0 failures, worst deviation
|
|
13
|
+
2. TypeScript port verified against Python golden fixtures: **3,218 checks,
|
|
14
|
+
0 failures, worst deviation 0.41 nano-arcseconds.** The two implementations
|
|
15
15
|
are numerically identical.
|
|
16
16
|
|
|
17
17
|
Regenerate fixtures any time from the Python side; any future TS change must
|
package/accuracy.json
CHANGED
|
@@ -107,9 +107,9 @@
|
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
"name": "Sidereal longitudes",
|
|
110
|
-
"max": "
|
|
110
|
+
"max": "—",
|
|
111
111
|
"rms": "—",
|
|
112
|
-
"note": "ayanamsa model vs SE ≤0.
|
|
112
|
+
"note": "ayanamsa model vs SE ≤0.005″ (Vondrák 2011, same model both sides); sidereal longitudes inherit each body's tropical bound"
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "RA / Dec",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
"name": "Topocentric Moon",
|
|
122
|
-
"max": "2.
|
|
122
|
+
"max": "2.5",
|
|
123
123
|
"rms": "—",
|
|
124
124
|
"note": "parallax model adds ≤0.1″ over the geocentric bound"
|
|
125
125
|
},
|
|
@@ -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.3",
|
|
183
|
+
"rms": "—",
|
|
184
|
+
"note": "HYG-derived catalog (ICRS J2000 + proper motions, full 3D space motion, Vondrák 2011 precession); vs swe_fixstar fed the same rows"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "Star-anchored ayanamsas",
|
|
188
|
+
"max": "0.5",
|
|
189
|
+
"rms": "—",
|
|
190
|
+
"note": "galcent_0sag and true_citra computed from the apparent star; bound tracks the fixed-star chain (≤0.3″) plus the body's tropical accuracy"
|
|
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.3″"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"label": "Sidereal (7 ayanamsas)",
|
|
264
|
+
"bound": "≤ 0.005″ 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,
|
|
@@ -67,7 +69,8 @@ export declare function vsopHeliocentric(series: VsopSeries, jde: number): [numb
|
|
|
67
69
|
export declare function nutation(data: EngineData, jde: number): [number, number];
|
|
68
70
|
export declare function meanObliquity(jde: number): number;
|
|
69
71
|
export declare function trueObliquity(data: EngineData, jde: number): number;
|
|
70
|
-
/** Precession of ecliptic coordinates (
|
|
72
|
+
/** Precession of ecliptic coordinates between epochs (Vondrak 2011):
|
|
73
|
+
* ecliptic-of-from -> J2000 equatorial -> ecliptic-of-to. */
|
|
71
74
|
export declare function precessEcliptic(lon: number, lat: number, jdeFrom: number, jdeTo: number): [number, number];
|
|
72
75
|
/** Apparent geocentric ecliptic lon/lat (true equinox of date), distance. */
|
|
73
76
|
export declare function planetApparent(data: EngineData, name: string, jde: number): [number, number, number];
|
|
@@ -94,9 +97,9 @@ export declare function trueNodeSeries(data: EngineData, jde: number): number;
|
|
|
94
97
|
/** Ecliptic lon/lat -> right ascension, declination (all radians). */
|
|
95
98
|
export declare function equatorial(lon: number, lat: number, eps: number): [number, number];
|
|
96
99
|
/** Mean ayanamsa at J2000.0 (degrees) per mode. Standard epoch anchors
|
|
97
|
-
* (matched to Swiss Ephemeris 2.10 to 1e-9 deg); propagation uses
|
|
98
|
-
* ecliptic precession
|
|
99
|
-
*
|
|
100
|
+
* (matched to Swiss Ephemeris 2.10 to 1e-9 deg); propagation uses Vondrak
|
|
101
|
+
* 2011 ecliptic precession, the same model Swiss Ephemeris uses:
|
|
102
|
+
* agreement over 1900-2099 is <=0.005 arcsec. */
|
|
100
103
|
export declare const AYANAMSA_J2000: Record<string, number>;
|
|
101
104
|
/** Mean ayanamsa in degrees. Sidereal longitude = (tropical true-equinox
|
|
102
105
|
* longitude - nutation in longitude) - ayanamsa: the sidereal zodiac is
|
package/dist/src/core.js
CHANGED
|
@@ -140,48 +140,138 @@ function fk5Correction(L, B, jde) {
|
|
|
140
140
|
const dB = 0.03916 * ARCSEC * (Math.cos(Lp) - Math.sin(Lp));
|
|
141
141
|
return [L + dL, B + dB];
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
// ------------------------------------------------- Vondrak 2011 precession
|
|
144
|
+
// Long-term precession of the ecliptic and equator (Vondrak, Capitaine &
|
|
145
|
+
// Wallace 2011, A&A 534 A22; coefficient tables as carried by ERFA under
|
|
146
|
+
// BSD-3). Replaces the IAU 1976 angles.
|
|
147
|
+
const PQ_POL = [
|
|
148
|
+
[5851.607687, -0.1189, -0.00028913, 0.000000101],
|
|
149
|
+
[-1600.8863, 1.1689818, -0.0000002, -0.000000437],
|
|
150
|
+
];
|
|
151
|
+
const PQ_PER = [
|
|
152
|
+
[708.15, -5486.751211, -684.66156, 667.66673, -5523.863691],
|
|
153
|
+
[2309.0, -17.127623, 2446.28388, -2354.886252, -549.74745],
|
|
154
|
+
[1620.0, -617.517403, 399.671049, -428.152441, -310.998056],
|
|
155
|
+
[492.2, 413.44294, -356.652376, 376.202861, 421.535876],
|
|
156
|
+
[1183.0, 78.614193, -186.387003, 184.778874, -36.776172],
|
|
157
|
+
[622.0, -180.732815, -316.80007, 335.321713, -145.278396],
|
|
158
|
+
[882.0, -87.676083, 198.296701, -185.138669, -34.74445],
|
|
159
|
+
[547.0, 46.140315, 101.135679, -120.97283, 22.885731],
|
|
160
|
+
];
|
|
161
|
+
const XY_POL = [
|
|
162
|
+
[5453.282155, 0.4252841, -0.00037173, -0.000000152],
|
|
163
|
+
[-73750.93035, -0.7675452, -0.00018725, 0.000000231],
|
|
164
|
+
];
|
|
165
|
+
const XY_PER = [
|
|
166
|
+
[256.75, -819.940624, 75004.344875, 81491.287984, 1558.515853],
|
|
167
|
+
[708.15, -8444.676815, 624.033993, 787.163481, 7774.939698],
|
|
168
|
+
[274.2, 2600.009459, 1251.136893, 1251.296102, -2219.534038],
|
|
169
|
+
[241.45, 2755.17563, -1102.212834, -1257.950837, -2523.969396],
|
|
170
|
+
[2309.0, -167.659835, -2660.66498, -2966.79973, 247.850422],
|
|
171
|
+
[492.2, 871.855056, 699.291817, 639.744522, -846.485643],
|
|
172
|
+
[396.1, 44.769698, 153.16722, 131.600209, -1393.124055],
|
|
173
|
+
[288.9, -512.313065, -950.865637, -445.040117, 368.526116],
|
|
174
|
+
[231.1, -819.415595, 499.754645, 584.522874, 749.045012],
|
|
175
|
+
[1610.0, -538.071099, -145.18821, -89.756563, 444.704518],
|
|
176
|
+
[620.0, -189.793622, 558.116553, 524.42963, 235.934465],
|
|
177
|
+
[157.87, -402.922932, -23.923029, -13.549067, 374.049623],
|
|
178
|
+
[220.3, 179.516345, -165.405086, -210.157124, -171.33018],
|
|
179
|
+
[1200.0, -9.814756, 9.344131, -44.919798, -22.899655],
|
|
180
|
+
];
|
|
181
|
+
const EPS0_V = 84381.406 * ARCSEC; // J2000 obliquity of the Vondrak model
|
|
182
|
+
const EPS0_FRAME = 84381.448 * ARCSEC; // obliquity defining our ecliptic-J2000 data
|
|
183
|
+
function ltpPecl(jde) {
|
|
184
|
+
const t = (jde - J2000) / 36525.0;
|
|
185
|
+
let p = 0.0;
|
|
186
|
+
let q = 0.0;
|
|
187
|
+
const w = 2.0 * Math.PI * t;
|
|
188
|
+
for (const [per, c1, c2, s1, s2] of PQ_PER) {
|
|
189
|
+
const a = w / per;
|
|
190
|
+
const ca = Math.cos(a);
|
|
191
|
+
const sa = Math.sin(a);
|
|
192
|
+
p += ca * c1 + sa * s1;
|
|
193
|
+
q += ca * c2 + sa * s2;
|
|
194
|
+
}
|
|
195
|
+
let tn = 1.0;
|
|
196
|
+
for (let i = 0; i < 4; i++) {
|
|
197
|
+
p += PQ_POL[0][i] * tn;
|
|
198
|
+
q += PQ_POL[1][i] * tn;
|
|
199
|
+
tn *= t;
|
|
200
|
+
}
|
|
201
|
+
p *= ARCSEC;
|
|
202
|
+
q *= ARCSEC;
|
|
203
|
+
const z = Math.sqrt(Math.max(1.0 - p * p - q * q, 0.0));
|
|
204
|
+
const s = Math.sin(EPS0_V);
|
|
205
|
+
const c = Math.cos(EPS0_V);
|
|
206
|
+
return [p, -q * c - z * s, -q * s + z * c];
|
|
207
|
+
}
|
|
208
|
+
function ltpPequ(jde) {
|
|
209
|
+
const t = (jde - J2000) / 36525.0;
|
|
210
|
+
let x = 0.0;
|
|
211
|
+
let y = 0.0;
|
|
212
|
+
const w = 2.0 * Math.PI * t;
|
|
213
|
+
for (const [per, c1, c2, s1, s2] of XY_PER) {
|
|
214
|
+
const a = w / per;
|
|
215
|
+
const ca = Math.cos(a);
|
|
216
|
+
const sa = Math.sin(a);
|
|
217
|
+
x += ca * c1 + sa * s1;
|
|
218
|
+
y += ca * c2 + sa * s2;
|
|
219
|
+
}
|
|
220
|
+
let tn = 1.0;
|
|
221
|
+
for (let i = 0; i < 4; i++) {
|
|
222
|
+
x += XY_POL[0][i] * tn;
|
|
223
|
+
y += XY_POL[1][i] * tn;
|
|
224
|
+
tn *= t;
|
|
225
|
+
}
|
|
226
|
+
x *= ARCSEC;
|
|
227
|
+
y *= ARCSEC;
|
|
228
|
+
return [x, y, Math.sqrt(Math.max(1.0 - x * x - y * y, 0.0))];
|
|
229
|
+
}
|
|
230
|
+
/** Rows of the rotation J2000-equatorial -> mean ecliptic/equinox of date
|
|
231
|
+
* (ERFA eraLtecm): x = equinox, z = ecliptic pole, y = z cross x. */
|
|
232
|
+
function ltpEclMatrix(jde) {
|
|
233
|
+
const p = ltpPequ(jde);
|
|
234
|
+
const z = ltpPecl(jde);
|
|
235
|
+
const wx = [
|
|
236
|
+
p[1] * z[2] - p[2] * z[1], p[2] * z[0] - p[0] * z[2], p[0] * z[1] - p[1] * z[0],
|
|
237
|
+
];
|
|
238
|
+
const n = Math.sqrt(wx[0] ** 2 + wx[1] ** 2 + wx[2] ** 2);
|
|
239
|
+
const x = [wx[0] / n, wx[1] / n, wx[2] / n];
|
|
240
|
+
const y = [
|
|
241
|
+
z[1] * x[2] - z[2] * x[1], z[2] * x[0] - z[0] * x[2], z[0] * x[1] - z[1] * x[0],
|
|
242
|
+
];
|
|
243
|
+
return [x, y, z];
|
|
244
|
+
}
|
|
245
|
+
/** Precession of ecliptic coordinates between epochs (Vondrak 2011):
|
|
246
|
+
* ecliptic-of-from -> J2000 equatorial -> ecliptic-of-to. */
|
|
144
247
|
export function precessEcliptic(lon, lat, jdeFrom, jdeTo) {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
248
|
+
const cb = Math.cos(lat);
|
|
249
|
+
const v = [cb * Math.cos(lon), cb * Math.sin(lon), Math.sin(lat)];
|
|
250
|
+
const [xf, yf, zf] = ltpEclMatrix(jdeFrom);
|
|
251
|
+
const e = [0, 1, 2].map((i) => xf[i] * v[0] + yf[i] * v[1] + zf[i] * v[2]);
|
|
252
|
+
const [xt, yt, zt] = ltpEclMatrix(jdeTo);
|
|
253
|
+
const u = [
|
|
254
|
+
xt[0] * e[0] + xt[1] * e[1] + xt[2] * e[2],
|
|
255
|
+
yt[0] * e[0] + yt[1] * e[1] + yt[2] * e[2],
|
|
256
|
+
zt[0] * e[0] + zt[1] * e[1] + zt[2] * e[2],
|
|
257
|
+
];
|
|
258
|
+
return [mod(Math.atan2(u[1], u[0]), TWO_PI),
|
|
259
|
+
Math.asin(Math.max(-1, Math.min(1, u[2])))];
|
|
260
|
+
}
|
|
261
|
+
/** Rotate a vector from the ecliptic-J2000 data frame (obliquity 84381.448
|
|
262
|
+
* arcsec, as used by Horizons/Meeus) to the mean ecliptic of date
|
|
263
|
+
* (Vondrak 2011). */
|
|
161
264
|
function eclJ2000ToEclDate(v, jde) {
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
[x, y] = [c * x + s * y, -s * x + c * y];
|
|
173
|
-
};
|
|
174
|
-
const ry = (a) => {
|
|
175
|
-
const c = Math.cos(a);
|
|
176
|
-
const s = Math.sin(a);
|
|
177
|
-
[x, z] = [c * x - s * z, s * x + c * z];
|
|
178
|
-
};
|
|
179
|
-
rz(-zeta);
|
|
180
|
-
ry(th);
|
|
181
|
-
rz(-zz);
|
|
182
|
-
const e = meanObliquity(jde);
|
|
183
|
-
[y, z] = [y * Math.cos(e) + z * Math.sin(e), -y * Math.sin(e) + z * Math.cos(e)];
|
|
184
|
-
return [x, y, z];
|
|
265
|
+
const [x, y, z] = v;
|
|
266
|
+
const s = Math.sin(EPS0_FRAME);
|
|
267
|
+
const c = Math.cos(EPS0_FRAME);
|
|
268
|
+
const e = [x, y * c - z * s, y * s + z * c];
|
|
269
|
+
const [xt, yt, zt] = ltpEclMatrix(jde);
|
|
270
|
+
return [
|
|
271
|
+
xt[0] * e[0] + xt[1] * e[1] + xt[2] * e[2],
|
|
272
|
+
yt[0] * e[0] + yt[1] * e[1] + yt[2] * e[2],
|
|
273
|
+
zt[0] * e[0] + zt[1] * e[1] + zt[2] * e[2],
|
|
274
|
+
];
|
|
185
275
|
}
|
|
186
276
|
// ---------------------------------------------------------------- planets
|
|
187
277
|
function geoVector(data, name, jde) {
|
|
@@ -385,9 +475,9 @@ export function equatorial(lon, lat, eps) {
|
|
|
385
475
|
return [ra, dec];
|
|
386
476
|
}
|
|
387
477
|
/** Mean ayanamsa at J2000.0 (degrees) per mode. Standard epoch anchors
|
|
388
|
-
* (matched to Swiss Ephemeris 2.10 to 1e-9 deg); propagation uses
|
|
389
|
-
* ecliptic precession
|
|
390
|
-
*
|
|
478
|
+
* (matched to Swiss Ephemeris 2.10 to 1e-9 deg); propagation uses Vondrak
|
|
479
|
+
* 2011 ecliptic precession, the same model Swiss Ephemeris uses:
|
|
480
|
+
* agreement over 1900-2099 is <=0.005 arcsec. */
|
|
391
481
|
export const AYANAMSA_J2000 = {
|
|
392
482
|
lahiri: 23.857092325,
|
|
393
483
|
fagan_bradley: 24.740299966,
|
|
@@ -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,32 @@
|
|
|
1
|
+
import { Engine, BodyId, Zodiac } from "./chart.js";
|
|
2
|
+
export declare const QUERY_ASPECTS: Record<string, number>;
|
|
3
|
+
export type Interval = [number, number];
|
|
4
|
+
/** Margin function (true where >= 0) carrying the bodies it depends on. */
|
|
5
|
+
export interface Predicate {
|
|
6
|
+
(engine: Engine, t: number): number;
|
|
7
|
+
bodies: Set<string>;
|
|
8
|
+
}
|
|
9
|
+
/** True while `body` is within `orb` deg of an exact `kind` aspect to
|
|
10
|
+
* `target` -- a fixed ecliptic longitude (deg) or another body name. */
|
|
11
|
+
export declare function aspect(body: BodyId, kind: string, target: number | BodyId, orb?: number, zodiac?: Zodiac): Predicate;
|
|
12
|
+
/** True while `body` is in `sign` (index 0=Aries..11=Pisces, or name). */
|
|
13
|
+
export declare function inSign(body: BodyId, sign: number | string, zodiac?: Zodiac): Predicate;
|
|
14
|
+
/** True while `body` is in apparent retrograde motion. */
|
|
15
|
+
export declare function retrograde(body: BodyId, zodiac?: Zodiac): Predicate;
|
|
16
|
+
/** True while `body` is direct or stationary. */
|
|
17
|
+
export declare function notRetrograde(body: BodyId, zodiac?: Zodiac): Predicate;
|
|
18
|
+
/** True where every predicate is true (interval intersection). */
|
|
19
|
+
export declare function allOf(...preds: Predicate[]): Predicate;
|
|
20
|
+
/** True where any predicate is true (interval union). */
|
|
21
|
+
export declare function anyOf(...preds: Predicate[]): Predicate;
|
|
22
|
+
/** True where `pred` is false (interval complement). */
|
|
23
|
+
export declare function notOf(pred: Predicate): Predicate;
|
|
24
|
+
export interface WhenOptions {
|
|
25
|
+
step?: number;
|
|
26
|
+
maxIntervals?: number;
|
|
27
|
+
}
|
|
28
|
+
/** Time intervals (jdStartUt, jdEndUt) in [jdStart, jdEnd] where `predicate`
|
|
29
|
+
* is true. Endpoints touching the range bounds are clamped. The scan step
|
|
30
|
+
* defaults to 0.125 d when a fast body (Moon, nodes, Lilith) is involved
|
|
31
|
+
* and 1 d otherwise. */
|
|
32
|
+
export declare function when(engine: Engine, predicate: Predicate, jdStart: number, jdEnd: number, opts?: WhenOptions): Interval[];
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine query -- declarative time queries ("when is ...?").
|
|
3
|
+
*
|
|
4
|
+
* The engine answers "where is the body?"; this answers "when is the
|
|
5
|
+
* configuration true?" over a time range. A predicate is a continuous
|
|
6
|
+
* "margin" function, true exactly where margin >= 0 (e.g. aspect-within-orb
|
|
7
|
+
* -> orb minus angular distance from exact). A boolean combination is then
|
|
8
|
+
* itself a margin -- AND = min of the parts, OR = max, NOT = negation -- so
|
|
9
|
+
* any query reduces to one continuous function and `when()` returns the
|
|
10
|
+
* intervals where it is true using the same coarse-scan-then-bisect root
|
|
11
|
+
* finder as events.crossings.
|
|
12
|
+
*
|
|
13
|
+
* when(engine, allOf(aspect("saturn", "square", natalMoon),
|
|
14
|
+
* notRetrograde("mercury"),
|
|
15
|
+
* inSign("venus", "Taurus")), jdStart, jdEnd)
|
|
16
|
+
*
|
|
17
|
+
* Mirrors the Python reference (astroengine/query.py); the golden fixtures
|
|
18
|
+
* pin the two implementations together.
|
|
19
|
+
*/
|
|
20
|
+
import { mod } from "./core.js";
|
|
21
|
+
import { SIGNS } from "./chart.js";
|
|
22
|
+
export const QUERY_ASPECTS = {
|
|
23
|
+
conjunction: 0, semisextile: 30, sextile: 60, square: 90,
|
|
24
|
+
trine: 120, quincunx: 150, opposition: 180,
|
|
25
|
+
};
|
|
26
|
+
const FAST = new Set([
|
|
27
|
+
"moon", "mean_node", "true_node", "mean_lilith", "true_lilith",
|
|
28
|
+
]);
|
|
29
|
+
function wrap180(d) {
|
|
30
|
+
return mod(d + 180, 360) - 180;
|
|
31
|
+
}
|
|
32
|
+
function mk(fn, bodies) {
|
|
33
|
+
const p = fn;
|
|
34
|
+
p.bodies = bodies;
|
|
35
|
+
return p;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------- predicates
|
|
38
|
+
/** True while `body` is within `orb` deg of an exact `kind` aspect to
|
|
39
|
+
* `target` -- a fixed ecliptic longitude (deg) or another body name. */
|
|
40
|
+
export function aspect(body, kind, target, orb = 1.0, zodiac = "tropical") {
|
|
41
|
+
const ang = QUERY_ASPECTS[kind];
|
|
42
|
+
if (ang === undefined)
|
|
43
|
+
throw new Error(`unknown aspect ${kind}`);
|
|
44
|
+
const isLon = typeof target === "number";
|
|
45
|
+
const bodies = new Set([body]);
|
|
46
|
+
if (!isLon)
|
|
47
|
+
bodies.add(target);
|
|
48
|
+
return mk((engine, t) => {
|
|
49
|
+
const lon = engine.longitude(body, t, { zodiac });
|
|
50
|
+
const tl = isLon
|
|
51
|
+
? target
|
|
52
|
+
: engine.longitude(target, t, { zodiac });
|
|
53
|
+
const sep = lon - tl;
|
|
54
|
+
return orb - Math.min(Math.abs(wrap180(sep - ang)), Math.abs(wrap180(sep + ang)));
|
|
55
|
+
}, bodies);
|
|
56
|
+
}
|
|
57
|
+
/** True while `body` is in `sign` (index 0=Aries..11=Pisces, or name). */
|
|
58
|
+
export function inSign(body, sign, zodiac = "tropical") {
|
|
59
|
+
const idx = typeof sign === "number" ? sign : SIGNS.indexOf(sign);
|
|
60
|
+
if (idx < 0)
|
|
61
|
+
throw new Error(`unknown sign ${sign}`);
|
|
62
|
+
const lo = idx * 30;
|
|
63
|
+
return mk((engine, t) => {
|
|
64
|
+
const d = mod(engine.longitude(body, t, { zodiac }) - lo, 360);
|
|
65
|
+
// signed distance to the nearest 30-deg band edge, positive inside
|
|
66
|
+
return d <= 30 ? Math.min(d, 30 - d) : -Math.min(d - 30, 360 - d);
|
|
67
|
+
}, new Set([body]));
|
|
68
|
+
}
|
|
69
|
+
/** True while `body` is in apparent retrograde motion. */
|
|
70
|
+
export function retrograde(body, zodiac = "tropical") {
|
|
71
|
+
const h = 0.25;
|
|
72
|
+
return mk((engine, t) => {
|
|
73
|
+
const l0 = engine.longitude(body, t - h, { zodiac });
|
|
74
|
+
const l1 = engine.longitude(body, t + h, { zodiac });
|
|
75
|
+
return -wrap180(l1 - l0) / (2 * h); // >= 0 when moving backwards
|
|
76
|
+
}, new Set([body]));
|
|
77
|
+
}
|
|
78
|
+
/** True while `body` is direct or stationary. */
|
|
79
|
+
export function notRetrograde(body, zodiac = "tropical") {
|
|
80
|
+
return notOf(retrograde(body, zodiac));
|
|
81
|
+
}
|
|
82
|
+
// --------------------------------------------------------------- combinators
|
|
83
|
+
function combine(op, preds) {
|
|
84
|
+
const bodies = new Set();
|
|
85
|
+
for (const p of preds)
|
|
86
|
+
for (const b of p.bodies)
|
|
87
|
+
bodies.add(b);
|
|
88
|
+
return mk((engine, t) => op(preds.map((p) => p(engine, t))), bodies);
|
|
89
|
+
}
|
|
90
|
+
/** True where every predicate is true (interval intersection). */
|
|
91
|
+
export function allOf(...preds) {
|
|
92
|
+
return combine((xs) => Math.min(...xs), preds);
|
|
93
|
+
}
|
|
94
|
+
/** True where any predicate is true (interval union). */
|
|
95
|
+
export function anyOf(...preds) {
|
|
96
|
+
return combine((xs) => Math.max(...xs), preds);
|
|
97
|
+
}
|
|
98
|
+
/** True where `pred` is false (interval complement). */
|
|
99
|
+
export function notOf(pred) {
|
|
100
|
+
return mk((engine, t) => -pred(engine, t), new Set(pred.bodies));
|
|
101
|
+
}
|
|
102
|
+
// --------------------------------------------------------------- solver
|
|
103
|
+
function bisect(f, a, b, tol = 1e-6) {
|
|
104
|
+
let fa = f(a);
|
|
105
|
+
for (let i = 0; i < 60; i++) {
|
|
106
|
+
const m = 0.5 * (a + b);
|
|
107
|
+
if (Math.abs(b - a) < tol)
|
|
108
|
+
return m;
|
|
109
|
+
const fm = f(m);
|
|
110
|
+
if ((fa < 0) !== (fm < 0)) {
|
|
111
|
+
b = m;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
a = m;
|
|
115
|
+
fa = fm;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return 0.5 * (a + b);
|
|
119
|
+
}
|
|
120
|
+
/** Time intervals (jdStartUt, jdEndUt) in [jdStart, jdEnd] where `predicate`
|
|
121
|
+
* is true. Endpoints touching the range bounds are clamped. The scan step
|
|
122
|
+
* defaults to 0.125 d when a fast body (Moon, nodes, Lilith) is involved
|
|
123
|
+
* and 1 d otherwise. */
|
|
124
|
+
export function when(engine, predicate, jdStart, jdEnd, opts = {}) {
|
|
125
|
+
let step = opts.step;
|
|
126
|
+
if (step === undefined) {
|
|
127
|
+
let fast = false;
|
|
128
|
+
for (const b of predicate.bodies)
|
|
129
|
+
if (FAST.has(b))
|
|
130
|
+
fast = true;
|
|
131
|
+
step = fast ? 0.125 : 1.0;
|
|
132
|
+
}
|
|
133
|
+
const maxIntervals = opts.maxIntervals ?? 500;
|
|
134
|
+
const f = (t) => predicate(engine, t);
|
|
135
|
+
const intervals = [];
|
|
136
|
+
let prev = f(jdStart);
|
|
137
|
+
let openStart = prev >= 0 ? jdStart : null;
|
|
138
|
+
let t = jdStart + step;
|
|
139
|
+
while (t <= jdEnd + 1e-9 && intervals.length < maxIntervals) {
|
|
140
|
+
if (t > jdEnd)
|
|
141
|
+
t = jdEnd;
|
|
142
|
+
const cur = f(t);
|
|
143
|
+
if ((prev < 0) !== (cur < 0)) {
|
|
144
|
+
const edge = bisect(f, t - step, t);
|
|
145
|
+
if (cur >= 0) {
|
|
146
|
+
openStart = edge;
|
|
147
|
+
}
|
|
148
|
+
else if (openStart !== null) {
|
|
149
|
+
intervals.push([openStart, edge]);
|
|
150
|
+
openStart = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
prev = cur;
|
|
154
|
+
if (t >= jdEnd)
|
|
155
|
+
break;
|
|
156
|
+
t += step;
|
|
157
|
+
}
|
|
158
|
+
if (openStart !== null)
|
|
159
|
+
intervals.push([openStart, jdEnd]);
|
|
160
|
+
return intervals;
|
|
161
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 -> Vondrak 2011
|
|
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.3 arcsec over 1900-2099.
|
|
10
|
+
*/
|
|
11
|
+
import { EngineData } from "./core.js";
|
|
12
|
+
export interface StarEntry {
|
|
13
|
+
ra: number;
|
|
14
|
+
dec: number;
|
|
15
|
+
pmra: number;
|
|
16
|
+
pmdec: number;
|
|
17
|
+
rv: number;
|
|
18
|
+
plx: number;
|
|
19
|
+
mag: number;
|
|
20
|
+
bayer: string;
|
|
21
|
+
}
|
|
22
|
+
export interface StarPack {
|
|
23
|
+
provenance: string;
|
|
24
|
+
frame: string;
|
|
25
|
+
stars: Record<string, StarEntry>;
|
|
26
|
+
}
|
|
27
|
+
/** Apparent ecliptic [lon, lat] of date (rad) for a catalog entry. */
|
|
28
|
+
export declare function starApparent(data: EngineData, s: StarEntry, jde: number): [number, number];
|
|
@@ -0,0 +1,52 @@
|
|
|
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 -> Vondrak 2011
|
|
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.3 arcsec over 1900-2099.
|
|
10
|
+
*/
|
|
11
|
+
import { DEG, ARCSEC, J2000, mod, nutation, precessEcliptic, vsopHeliocentric, } from "./core.js";
|
|
12
|
+
const TWO_PI = 2 * Math.PI;
|
|
13
|
+
const KM_PER_AU = 149597870.7;
|
|
14
|
+
const AU_PER_PC = 206264.806;
|
|
15
|
+
/** Apparent ecliptic [lon, lat] of date (rad) for a catalog entry. */
|
|
16
|
+
export function starApparent(data, s, jde) {
|
|
17
|
+
const t = (jde - J2000) / 365.25;
|
|
18
|
+
const ra = s.ra * DEG;
|
|
19
|
+
const dec = s.dec * DEG;
|
|
20
|
+
const rAu = s.plx > 0 ? AU_PER_PC / (s.plx * 1e-3) : 1e9 * AU_PER_PC;
|
|
21
|
+
const cd = Math.cos(dec);
|
|
22
|
+
const sd = Math.sin(dec);
|
|
23
|
+
const cr = Math.cos(ra);
|
|
24
|
+
const sr = Math.sin(ra);
|
|
25
|
+
const p = [cd * cr, cd * sr, sd];
|
|
26
|
+
const east = [-sr, cr, 0.0];
|
|
27
|
+
const north = [-sd * cr, -sd * sr, cd];
|
|
28
|
+
const pmra = s.pmra * 1e-3 * ARCSEC;
|
|
29
|
+
const pmdec = s.pmdec * 1e-3 * ARCSEC;
|
|
30
|
+
const rv = (s.rv * 86400 * 365.25) / KM_PER_AU;
|
|
31
|
+
const pos = [0, 1, 2].map((i) => p[i] * rAu + (east[i] * pmra * rAu + north[i] * pmdec * rAu + p[i] * rv) * t);
|
|
32
|
+
const rn = Math.sqrt(pos[0] ** 2 + pos[1] ** 2 + pos[2] ** 2);
|
|
33
|
+
const x = pos[0] / rn;
|
|
34
|
+
const y = pos[1] / rn;
|
|
35
|
+
const z = pos[2] / rn;
|
|
36
|
+
const ra2 = Math.atan2(y, x);
|
|
37
|
+
const dec2 = Math.asin(z);
|
|
38
|
+
const e0 = 84381.448 * ARCSEC;
|
|
39
|
+
let lat = Math.asin(Math.sin(dec2) * Math.cos(e0) - Math.cos(dec2) * Math.sin(e0) * Math.sin(ra2));
|
|
40
|
+
let lon = mod(Math.atan2(Math.sin(ra2) * Math.cos(e0) + Math.tan(dec2) * Math.sin(e0), Math.cos(ra2)), TWO_PI);
|
|
41
|
+
[lon, lat] = precessEcliptic(lon, lat, J2000, jde);
|
|
42
|
+
const [L0] = vsopHeliocentric(data.vsop.earth, jde);
|
|
43
|
+
const sunLon = mod(L0 + Math.PI, TWO_PI);
|
|
44
|
+
const T = (jde - J2000) / 36525.0;
|
|
45
|
+
const k = 20.4898 * ARCSEC;
|
|
46
|
+
const e = 0.016708634 - 0.000042037 * T;
|
|
47
|
+
const piPer = (102.93735 + 1.71946 * T) * DEG;
|
|
48
|
+
lon += (-k * Math.cos(sunLon - lon) + e * k * Math.cos(piPer - lon)) / Math.cos(lat);
|
|
49
|
+
lat += -k * Math.sin(lat) * (Math.sin(sunLon - lon) - e * Math.sin(piPer - lon));
|
|
50
|
+
lon = mod(lon + nutation(data, jde)[0], TWO_PI);
|
|
51
|
+
return [lon, lat];
|
|
52
|
+
}
|