caelus 0.3.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 +13 -5
- package/accuracy.json +265 -35
- package/dist/src/chart.d.ts +21 -3
- package/dist/src/chart.js +94 -15
- package/dist/src/core.d.ts +39 -1
- package/dist/src/core.js +78 -0
- package/dist/src/eclipses.d.ts +24 -0
- package/dist/src/eclipses.js +163 -0
- package/dist/src/events.d.ts +29 -0
- package/dist/src/events.js +184 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/node-loader.js +24 -2
- package/dist/src/stars.d.ts +29 -0
- package/dist/src/stars.js +53 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ ephemeris files. 1:1 port of the Python reference, checked by golden fixtures.
|
|
|
6
6
|
## Verification chain
|
|
7
7
|
|
|
8
8
|
1. Python engine checked against Swiss Ephemeris 2.10 across 1900–2099:
|
|
9
|
-
every planet ≤ 1″ (Sun–Saturn), Moon ≤ 2.5″, Chiron ≤ 1″,
|
|
9
|
+
every planet ≤ 1″ (Sun–Saturn), Moon ≤ 2.5″, Chiron ≤ 1″, mean node ≤ 1″,
|
|
10
|
+
true node ≤ 1′ vs SE's built-in ephemeris (≤ 1″ vs JPL DE431)
|
|
10
11
|
(vs full DE431 files, 1850–2149), angles and Placidus cusps ≤ 3.2″ — all
|
|
11
12
|
invisible at the arcminute display precision chart software uses.
|
|
12
|
-
2. TypeScript port verified against Python golden fixtures: **3,
|
|
13
|
+
2. TypeScript port verified against Python golden fixtures: **3,218 checks,
|
|
13
14
|
0 failures, worst deviation 1.64 nano-arcseconds.** The two implementations
|
|
14
15
|
are numerically identical.
|
|
15
16
|
|
|
@@ -62,8 +63,15 @@ engine.longitude("chiron", 2451545.0);
|
|
|
62
63
|
engine.position("mars", 2451545.0); // { lon, speed, retrograde, sign, signDeg }
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
Bodies: sun, moon, mercury…pluto, chiron, mean_node, true_node
|
|
66
|
-
|
|
66
|
+
Bodies: sun, moon, mercury…pluto, chiron, mean_node, true_node; on request:
|
|
67
|
+
mean_lilith, true_lilith, ceres, pallas, juno, vesta, pholus, and the eight
|
|
68
|
+
Hamburg-school Uranian bodies (cupido…poseidon) when their data packs are
|
|
69
|
+
loaded.
|
|
70
|
+
House systems: placidus, porphyry, equal, whole_sign, koch, regiomontanus,
|
|
71
|
+
campanus, alcabitius, morinus, meridian, polich_page, vehlow (Placidus and
|
|
72
|
+
Koch fall back to whole_sign above the polar circles).
|
|
73
|
+
Event search: rise/set/meridian transits, zodiac crossings, lunar phases,
|
|
74
|
+
stations (`events.ts`).
|
|
67
75
|
Performance: ~2.4 ms per full chart (13 bodies × 3 evaluations + houses +
|
|
68
76
|
aspects) single-threaded in Node 22 — ~420 charts/sec, faster in hot loops.
|
|
69
77
|
|
|
@@ -92,4 +100,4 @@ test/golden.test.ts conformance suite vs Python fixtures
|
|
|
92
100
|
- caelus — this package
|
|
93
101
|
- [caelus-birth](https://www.npmjs.com/package/caelus-birth) — local birth time + place → UT (charts take UT; use this)
|
|
94
102
|
- [caelus-wheel](https://www.npmjs.com/package/caelus-wheel) — React SVG chart wheel
|
|
95
|
-
- [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server,
|
|
103
|
+
- [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, seven chart tools over stdio
|
package/accuracy.json
CHANGED
|
@@ -3,41 +3,271 @@
|
|
|
3
3
|
"range": "1900-2099",
|
|
4
4
|
"unit": "arcsec",
|
|
5
5
|
"bodies": [
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
6
|
+
{
|
|
7
|
+
"name": "Sun",
|
|
8
|
+
"max": "0.4",
|
|
9
|
+
"rms": "0.2",
|
|
10
|
+
"note": ""
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "Moon (precise tier)",
|
|
14
|
+
"max": "2.5",
|
|
15
|
+
"rms": "0.9",
|
|
16
|
+
"note": "JPL DE423 fit (2010); DE423 vs DE440 is <0.1″ over this span"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "Moon (embedded series)",
|
|
20
|
+
"max": "9.6",
|
|
21
|
+
"rms": "2.8",
|
|
22
|
+
"note": "60-term ELP abridged"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "Mercury",
|
|
26
|
+
"max": "0.5",
|
|
27
|
+
"rms": "0.2",
|
|
28
|
+
"note": ""
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "Venus",
|
|
32
|
+
"max": "0.8",
|
|
33
|
+
"rms": "0.2",
|
|
34
|
+
"note": ""
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "Mars",
|
|
38
|
+
"max": "0.7",
|
|
39
|
+
"rms": "0.2",
|
|
40
|
+
"note": ""
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "Jupiter",
|
|
44
|
+
"max": "0.9",
|
|
45
|
+
"rms": "0.3",
|
|
46
|
+
"note": ""
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "Saturn",
|
|
50
|
+
"max": "0.8",
|
|
51
|
+
"rms": "0.4",
|
|
52
|
+
"note": ""
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "Uranus",
|
|
56
|
+
"max": "1.9",
|
|
57
|
+
"rms": "0.7",
|
|
58
|
+
"note": "series truncation; complete VSOP87 holds ≤1″"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "Neptune",
|
|
62
|
+
"max": "4.6",
|
|
63
|
+
"rms": "2.2",
|
|
64
|
+
"note": "series truncation; complete VSOP87 holds ≤1″"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "Pluto",
|
|
68
|
+
"max": "2.5",
|
|
69
|
+
"rms": "1.0",
|
|
70
|
+
"note": "series valid 1885-2099"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "Chiron",
|
|
74
|
+
"max": "1.0",
|
|
75
|
+
"rms": "0.3",
|
|
76
|
+
"note": "JPL Horizons fit, 1850-2150"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "Mean node",
|
|
80
|
+
"max": "0.1",
|
|
81
|
+
"rms": "0.1",
|
|
82
|
+
"note": ""
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "True node",
|
|
86
|
+
"max": "0.8",
|
|
87
|
+
"rms": "0.4",
|
|
88
|
+
"note": "vs full JPL DE431 files; vs Swiss Ephemeris's BUILT-IN (Moshier) ephemeris expect up to ~1′ (measured: 50″ max, 8″ median, 300 epochs) — that is the built-in lunar theory's own node error, not ours"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "Ascendant / MC",
|
|
92
|
+
"max": "3.2",
|
|
93
|
+
"rms": "—",
|
|
94
|
+
"note": ""
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "Placidus cusps (all 12)",
|
|
98
|
+
"max": "3.2",
|
|
99
|
+
"rms": "—",
|
|
100
|
+
"note": ""
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "Mean Lilith",
|
|
104
|
+
"max": "1.3",
|
|
105
|
+
"rms": "0.5",
|
|
106
|
+
"note": "mean lunar apogee on the inclined orbit; latitude ≤0.1″"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "Sidereal longitudes",
|
|
110
|
+
"max": "0.1",
|
|
111
|
+
"rms": "—",
|
|
112
|
+
"note": "ayanamsa model vs SE ≤0.30″ at the 1900/2099 edges (IAU 1976 vs Vondrák precession); Sun worst-case 0.08″ at 120 sampled epochs"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "RA / Dec",
|
|
116
|
+
"max": "2.1",
|
|
117
|
+
"rms": "—",
|
|
118
|
+
"note": "rotation is exact; bound tracks each body's ecliptic accuracy (Moon worst)"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "Topocentric Moon",
|
|
122
|
+
"max": "2.7",
|
|
123
|
+
"rms": "—",
|
|
124
|
+
"note": "parallax model adds ≤0.1″ over the geocentric bound"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "House cusps, 8 new systems",
|
|
128
|
+
"max": "0.0",
|
|
129
|
+
"rms": "0.0",
|
|
130
|
+
"note": "Koch, Regiomontanus, Campanus, Alcabitius, Morinus, Meridian, Polich-Page, Vehlow: exact vs swe_houses_armc (200 polar-inclusive cases each)"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "Vertex / east point",
|
|
134
|
+
"max": "0.0",
|
|
135
|
+
"rms": "0.0",
|
|
136
|
+
"note": "exact vs swe_houses_armc"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "Magnitudes",
|
|
140
|
+
"max": "0.045 mag",
|
|
141
|
+
"rms": "—",
|
|
142
|
+
"note": "Mallama 2018; Moon (Allen law) valid to phase angle 140°"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "Rise/set/meridian transit",
|
|
146
|
+
"max": "0.5 s",
|
|
147
|
+
"rms": "—",
|
|
148
|
+
"note": "vs swe_rise_trans, 48 polar-inclusive cases per body; Moon bound tracks its position accuracy"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "Crossings / lunar phases",
|
|
152
|
+
"max": "4 s",
|
|
153
|
+
"rms": "—",
|
|
154
|
+
"note": "vs swe_solcross/swe_mooncross and elongation root-finds"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "Stations",
|
|
158
|
+
"max": "55 s",
|
|
159
|
+
"rms": "—",
|
|
160
|
+
"note": "ill-conditioned by nature: speed-zero slope ~0.01°/day² turns sub-arcsecond model differences into minutes"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "True Lilith (osc. apogee)",
|
|
164
|
+
"max": "187",
|
|
165
|
+
"rms": "—",
|
|
166
|
+
"note": "hypersensitive to the lunar theory (~1/e amplification); SE's own Moshier-vs-DE difference dominates. 'True Lilith' values disagree across software at this scale"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"name": "Ceres, Pallas, Juno, Vesta, Pholus",
|
|
170
|
+
"max": "1.0",
|
|
171
|
+
"rms": "0.3",
|
|
172
|
+
"note": "JPL Horizons fits 1850-2150 (residual <5e-6 AU); same geocentric pipeline as Chiron. No independent SE oracle here: SE's asteroid files are unavailable in Moshier mode"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "Uranian bodies (Cupido…Poseidon)",
|
|
176
|
+
"max": "2.3",
|
|
177
|
+
"rms": "—",
|
|
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"
|
|
203
|
+
}
|
|
29
204
|
],
|
|
30
205
|
"summary": [
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
206
|
+
{
|
|
207
|
+
"label": "Sun–Saturn",
|
|
208
|
+
"bound": "≤ 1″"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"label": "Uranus / Neptune",
|
|
212
|
+
"bound": "≤ 1.9″ / ≤ 4.6″"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"label": "Moon (1920–2080 tier)",
|
|
216
|
+
"bound": "≤ 2.5″"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"label": "Moon (series, embedded)",
|
|
220
|
+
"bound": "≤ 10″"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"label": "Pluto / Chiron",
|
|
224
|
+
"bound": "≤ 2.5″ / ≤ 1″"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"label": "Angles & Placidus cusps",
|
|
228
|
+
"bound": "≤ 3.2″"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"label": "True node",
|
|
232
|
+
"bound": "≤ 1′ vs SE built-in (≤ 1″ vs JPL DE431)"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"label": "Mean Lilith",
|
|
236
|
+
"bound": "≤ 1.3″"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"label": "8 new house systems",
|
|
240
|
+
"bound": "exact (0.0″)"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"label": "Rise/set/transit",
|
|
244
|
+
"bound": "≤ 0.5 s"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"label": "Crossings & phases",
|
|
248
|
+
"bound": "≤ 4 s"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"label": "Asteroids (big 4 + Pholus)",
|
|
252
|
+
"bound": "≤ 1″"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"label": "Uranian bodies",
|
|
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"
|
|
269
|
+
}
|
|
41
270
|
],
|
|
42
|
-
"v03_harness": "python/validate_swiss.py regenerates every figure above the line against pyswisseph 2.10 (Moshier mode)"
|
|
43
|
-
|
|
271
|
+
"v03_harness": "python/validate_swiss.py regenerates every figure above the line against pyswisseph 2.10 (Moshier mode)",
|
|
272
|
+
"true_node_vs_builtin": "1′"
|
|
273
|
+
}
|
package/dist/src/chart.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { EngineData, AYANAMSA_J2000 } from "./core.js";
|
|
|
3
3
|
export declare const BODIES: readonly ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", "chiron", "mean_node", "true_node"];
|
|
4
4
|
export type Body = (typeof BODIES)[number];
|
|
5
5
|
/** Computable on request (not in the default chart set). */
|
|
6
|
-
export declare const EXTRA_BODIES: readonly ["mean_lilith"];
|
|
6
|
+
export declare const EXTRA_BODIES: readonly ["mean_lilith", "true_lilith"];
|
|
7
7
|
/** Core names keep autocomplete; any string id is accepted (data packs). */
|
|
8
8
|
export type BodyId = Body | (typeof EXTRA_BODIES)[number] | (string & {});
|
|
9
9
|
export declare const SIGNS: string[];
|
|
@@ -68,12 +68,30 @@ export declare class Engine {
|
|
|
68
68
|
readonly data: EngineData;
|
|
69
69
|
private moonCheb;
|
|
70
70
|
private chironCheb;
|
|
71
|
+
private packs;
|
|
71
72
|
constructor(data: EngineData);
|
|
73
|
+
private pack;
|
|
72
74
|
private moonInRange;
|
|
73
75
|
/** Body ids this engine can compute, given the data it was handed. */
|
|
74
76
|
bodies(): BodyId[];
|
|
75
|
-
/** Apparent geocentric [lon rad, lat rad, dist AU | null].
|
|
76
|
-
|
|
77
|
+
/** Apparent geocentric [lon rad, lat rad, dist AU | null] at TT jde.
|
|
78
|
+
* Building block for the events module; chart consumers want
|
|
79
|
+
* position() instead. */
|
|
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[];
|
|
77
95
|
private lonOnly;
|
|
78
96
|
/** Apparent geocentric ecliptic longitude (deg). Tropical: true equinox
|
|
79
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
|
-
import { DEG, mod, jdTT, julianDay, ChebSeries, planetApparent, sunApparent, moonApparentSeries, moonApparentPrecise, plutoApparent, chironApparent, meanNode, trueNodeSeries, trueNodePrecise, equatorial, ayanamsa, AYANAMSA_J2000, meanLilith, topocentricEcl, trueObliquity, nutation, plutoHeliocentric, vsopHeliocentric, precessEcliptic, J2000, } from "./core.js";
|
|
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 = [
|
|
@@ -7,9 +8,11 @@ export const BODIES = [
|
|
|
7
8
|
"uranus", "neptune", "pluto", "chiron", "mean_node", "true_node",
|
|
8
9
|
];
|
|
9
10
|
/** Computable on request (not in the default chart set). */
|
|
10
|
-
export const EXTRA_BODIES = ["mean_lilith"];
|
|
11
|
+
export const EXTRA_BODIES = ["mean_lilith", "true_lilith"];
|
|
11
12
|
/** Points: excluded from aspect search by default. */
|
|
12
|
-
const NOT_ASPECTABLE = new Set([
|
|
13
|
+
const NOT_ASPECTABLE = new Set([
|
|
14
|
+
"mean_node", "true_node", "mean_lilith", "true_lilith",
|
|
15
|
+
]);
|
|
13
16
|
export const SIGNS = [
|
|
14
17
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra",
|
|
15
18
|
"Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
|
@@ -26,11 +29,18 @@ function parseZodiac(zodiac) {
|
|
|
26
29
|
return null;
|
|
27
30
|
if (zodiac.startsWith("sidereal:")) {
|
|
28
31
|
const mode = zodiac.slice("sidereal:".length);
|
|
29
|
-
if (AYANAMSA_J2000[mode] !== undefined)
|
|
32
|
+
if (AYANAMSA_J2000[mode] !== undefined || STAR_AYANAMSAS[mode])
|
|
30
33
|
return mode;
|
|
31
34
|
}
|
|
32
35
|
throw new Error(`unknown zodiac ${JSON.stringify(zodiac)}`);
|
|
33
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
|
+
};
|
|
34
44
|
const VSOP_BODIES = new Set([
|
|
35
45
|
"mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune",
|
|
36
46
|
]);
|
|
@@ -38,20 +48,42 @@ export class Engine {
|
|
|
38
48
|
data;
|
|
39
49
|
moonCheb;
|
|
40
50
|
chironCheb;
|
|
51
|
+
packs = new Map();
|
|
41
52
|
constructor(data) {
|
|
42
53
|
this.data = data;
|
|
43
54
|
this.moonCheb = data.moonCheb ? new ChebSeries(data.moonCheb) : null;
|
|
44
55
|
this.chironCheb = data.chiron ? new ChebSeries(data.chiron) : null;
|
|
45
56
|
}
|
|
57
|
+
pack(body) {
|
|
58
|
+
let s = this.packs.get(body);
|
|
59
|
+
if (!s) {
|
|
60
|
+
const raw = this.data.chebPacks?.[body];
|
|
61
|
+
const kp = this.data.keplerPack;
|
|
62
|
+
if (raw)
|
|
63
|
+
s = new ChebSeries(raw);
|
|
64
|
+
else if (kp?.bodies[body])
|
|
65
|
+
s = new KeplerOrbit(kp.bodies[body], kp.epoch);
|
|
66
|
+
else
|
|
67
|
+
throw new Error(`no data loaded for body '${body}'`);
|
|
68
|
+
this.packs.set(body, s);
|
|
69
|
+
}
|
|
70
|
+
return s;
|
|
71
|
+
}
|
|
46
72
|
moonInRange(jde) {
|
|
47
73
|
return !!this.moonCheb
|
|
48
74
|
&& this.moonCheb.jd0 <= jde - 0.1 && jde + 0.1 <= this.moonCheb.jd1;
|
|
49
75
|
}
|
|
50
76
|
/** Body ids this engine can compute, given the data it was handed. */
|
|
51
77
|
bodies() {
|
|
52
|
-
return [
|
|
78
|
+
return [
|
|
79
|
+
...[...BODIES, ...EXTRA_BODIES].filter((b) => b !== "chiron" || this.chironCheb),
|
|
80
|
+
...Object.keys(this.data.chebPacks ?? {}),
|
|
81
|
+
...Object.keys(this.data.keplerPack?.bodies ?? {}),
|
|
82
|
+
];
|
|
53
83
|
}
|
|
54
|
-
/** Apparent geocentric [lon rad, lat rad, dist AU | null].
|
|
84
|
+
/** Apparent geocentric [lon rad, lat rad, dist AU | null] at TT jde.
|
|
85
|
+
* Building block for the events module; chart consumers want
|
|
86
|
+
* position() instead. */
|
|
55
87
|
ecliptic(body, jde) {
|
|
56
88
|
if (body === "sun")
|
|
57
89
|
return sunApparent(this.data, jde);
|
|
@@ -82,10 +114,53 @@ export class Engine {
|
|
|
82
114
|
const [lon, lat] = meanLilith(this.data, jde);
|
|
83
115
|
return [lon, lat, null];
|
|
84
116
|
}
|
|
117
|
+
if (body === "true_lilith") {
|
|
118
|
+
const [lon, lat, km] = this.moonInRange(jde)
|
|
119
|
+
? oscApogeePrecise(this.data, this.moonCheb, jde)
|
|
120
|
+
: oscApogeeSeries(this.data, jde);
|
|
121
|
+
return [lon, lat, km / KM_PER_AU];
|
|
122
|
+
}
|
|
123
|
+
if (this.data.chebPacks?.[body] || this.data.keplerPack?.bodies[body]) {
|
|
124
|
+
// same heliocentric pipeline as Chiron (Chebyshev or Kepler source)
|
|
125
|
+
return chironApparent(this.data, this.pack(body), jde);
|
|
126
|
+
}
|
|
85
127
|
if (this.data.vsop[body])
|
|
86
128
|
return planetApparent(this.data, body, jde);
|
|
87
129
|
throw new Error(`no data loaded for body '${body}'`);
|
|
88
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
|
+
}
|
|
89
164
|
lonOnly(body, jdUt, mode, topo) {
|
|
90
165
|
const jde = jdTT(jdUt);
|
|
91
166
|
let [lon, lat, dist] = this.ecliptic(body, jde);
|
|
@@ -94,9 +169,8 @@ export class Engine {
|
|
|
94
169
|
[lon, lat, dist] = topocentricEcl(lon, lat, dist, lst, topo.lat * DEG, topo.altM ?? 0.0, trueObliquity(this.data, jde));
|
|
95
170
|
}
|
|
96
171
|
let lonDeg = lon / DEG;
|
|
97
|
-
if (mode !== null)
|
|
98
|
-
lonDeg = mod(lonDeg -
|
|
99
|
-
}
|
|
172
|
+
if (mode !== null)
|
|
173
|
+
lonDeg = mod(lonDeg - this.ayanShift(jde, mode), 360);
|
|
100
174
|
return lonDeg;
|
|
101
175
|
}
|
|
102
176
|
/** Apparent geocentric ecliptic longitude (deg). Tropical: true equinox
|
|
@@ -125,6 +199,13 @@ export class Engine {
|
|
|
125
199
|
b = Math.atan2(z, Math.hypot(x, y));
|
|
126
200
|
[l, b] = precessEcliptic(l, b, J2000, jde);
|
|
127
201
|
}
|
|
202
|
+
else if (this.data.chebPacks?.[body] || this.data.keplerPack?.bodies[body]) {
|
|
203
|
+
const [x, y, z] = this.pack(body).xyz(jde);
|
|
204
|
+
r = Math.sqrt(x * x + y * y + z * z);
|
|
205
|
+
l = mod(Math.atan2(y, x), TWO_PI);
|
|
206
|
+
b = Math.atan2(z, Math.hypot(x, y));
|
|
207
|
+
[l, b] = precessEcliptic(l, b, J2000, jde);
|
|
208
|
+
}
|
|
128
209
|
else if (VSOP_BODIES.has(body) && this.data.vsop[body]) {
|
|
129
210
|
[l, b, r] = vsopHeliocentric(this.data.vsop[body], jde);
|
|
130
211
|
}
|
|
@@ -145,9 +226,8 @@ export class Engine {
|
|
|
145
226
|
}
|
|
146
227
|
const [ra, dec] = equatorial(lonR, latR, trueObliquity(this.data, jde));
|
|
147
228
|
let lon = lonR / DEG;
|
|
148
|
-
if (mode !== null)
|
|
149
|
-
lon = mod(lon -
|
|
150
|
-
}
|
|
229
|
+
if (mode !== null)
|
|
230
|
+
lon = mod(lon - this.ayanShift(jde, mode), 360);
|
|
151
231
|
const h = 0.25; // days; central difference
|
|
152
232
|
const l0 = this.lonOnly(body, jdUt - h, mode, topo);
|
|
153
233
|
const l1 = this.lonOnly(body, jdUt + h, mode, topo);
|
|
@@ -235,9 +315,8 @@ export class Engine {
|
|
|
235
315
|
}
|
|
236
316
|
const jde = jdTT(jdUt);
|
|
237
317
|
let shift = 0.0;
|
|
238
|
-
if (mode !== null)
|
|
239
|
-
shift =
|
|
240
|
-
}
|
|
318
|
+
if (mode !== null)
|
|
319
|
+
shift = this.ayanShift(jde, mode);
|
|
241
320
|
const outDeg = (rad) => mod(rad / DEG - shift, 360);
|
|
242
321
|
let cuspsDeg;
|
|
243
322
|
if (mode !== null && used === "whole_sign") {
|
package/dist/src/core.d.ts
CHANGED
|
@@ -26,6 +26,24 @@ export type ChebData = {
|
|
|
26
26
|
scale?: number;
|
|
27
27
|
segments: number[][][];
|
|
28
28
|
};
|
|
29
|
+
export type KeplerElements = {
|
|
30
|
+
a: number;
|
|
31
|
+
e: number;
|
|
32
|
+
i: number;
|
|
33
|
+
node: number;
|
|
34
|
+
peri: number;
|
|
35
|
+
M0: number;
|
|
36
|
+
n: number;
|
|
37
|
+
};
|
|
38
|
+
export type KeplerPack = {
|
|
39
|
+
epoch: number;
|
|
40
|
+
bodies: Record<string, KeplerElements>;
|
|
41
|
+
};
|
|
42
|
+
/** Anything that yields heliocentric ecliptic-J2000 xyz (AU) at a TT jd:
|
|
43
|
+
* ChebSeries (fitted small bodies) or KeplerOrbit (Uranian bodies). */
|
|
44
|
+
export interface XyzSource {
|
|
45
|
+
xyz(jd: number): [number, number, number];
|
|
46
|
+
}
|
|
29
47
|
export interface EngineData {
|
|
30
48
|
vsop: Record<string, VsopSeries>;
|
|
31
49
|
nutation: number[][];
|
|
@@ -33,6 +51,13 @@ export interface EngineData {
|
|
|
33
51
|
pluto: number[][];
|
|
34
52
|
chiron?: ChebData;
|
|
35
53
|
moonCheb?: ChebData;
|
|
54
|
+
/** Heliocentric ecliptic-J2000 Chebyshev packs by body id (ceres,
|
|
55
|
+
* pallas, juno, vesta, pholus, ...). Same pipeline as Chiron. */
|
|
56
|
+
chebPacks?: Record<string, ChebData>;
|
|
57
|
+
/** Hamburg-school (Uranian) constant-element orbits; see fit_uranian.py. */
|
|
58
|
+
keplerPack?: KeplerPack;
|
|
59
|
+
/** Fixed-star catalog (HYG-derived; ICRS J2000 + proper motions). */
|
|
60
|
+
fixedStars?: import("./stars.js").StarPack;
|
|
36
61
|
}
|
|
37
62
|
export declare function julianDay(y: number, mo: number, d: number, h?: number, mi?: number, s?: number): number;
|
|
38
63
|
/** TT - UT1 in seconds. Observed IERS 1955-2025, E&M polynomials before,
|
|
@@ -82,6 +107,19 @@ export declare function ayanamsa(jde: number, mode: string): number;
|
|
|
82
107
|
/** Mean lunar apogee (Black Moon Lilith) on the inclined lunar orbit:
|
|
83
108
|
* apparent lon (true equinox) and orbital latitude, radians. */
|
|
84
109
|
export declare function meanLilith(data: EngineData, jde: number): [number, number];
|
|
110
|
+
/** Osculating lunar apogee (True Lilith) from the Chebyshev moon. */
|
|
111
|
+
export declare function oscApogeePrecise(data: EngineData, cheb: ChebSeries, jde: number): [number, number, number];
|
|
112
|
+
/** Series fallback outside the Chebyshev range (same finite-difference
|
|
113
|
+
* state as the true-node fallback). */
|
|
114
|
+
export declare function oscApogeeSeries(data: EngineData, jde: number): [number, number, number];
|
|
115
|
+
/** Constant-element two-body orbit with the same xyz(jde) interface as
|
|
116
|
+
* ChebSeries, so chironApparent takes either. */
|
|
117
|
+
export declare class KeplerOrbit implements XyzSource {
|
|
118
|
+
private els;
|
|
119
|
+
private epoch;
|
|
120
|
+
constructor(els: KeplerElements, epoch: number);
|
|
121
|
+
xyz(jde: number): [number, number, number];
|
|
122
|
+
}
|
|
85
123
|
export declare const EARTH_RADIUS_AU: number;
|
|
86
124
|
/** Diurnal parallax in ecliptic coordinates (Meeus ch. 11/40).
|
|
87
125
|
* lst = local apparent sidereal time (rad). Returns [lon, lat, distAu]. */
|
|
@@ -89,4 +127,4 @@ export declare function topocentricEcl(lon: number, lat: number, distAu: number,
|
|
|
89
127
|
/** Meeus ch.37 heliocentric Pluto, ecliptic J2000: [l rad, b rad, r AU]. */
|
|
90
128
|
export declare function plutoHeliocentric(data: EngineData, jde: number): [number, number, number];
|
|
91
129
|
export declare function plutoApparent(data: EngineData, jde: number): [number, number, number];
|
|
92
|
-
export declare function chironApparent(data: EngineData, cheb:
|
|
130
|
+
export declare function chironApparent(data: EngineData, cheb: XyzSource, jde: number): [number, number, number];
|
package/dist/src/core.js
CHANGED
|
@@ -420,6 +420,84 @@ export function meanLilith(data, jde) {
|
|
|
420
420
|
lon = mod(lon + nutation(data, jde)[0], TWO_PI);
|
|
421
421
|
return [lon, lat];
|
|
422
422
|
}
|
|
423
|
+
const GM_EARTH_MOON = 403503.2356 * 86400.0 ** 2; // km^3/day^2
|
|
424
|
+
/** Osculating apogee point from a geocentric lunar state vector (km,
|
|
425
|
+
* km/day): apparent ecliptic lon/lat of date (rad) + distance (km).
|
|
426
|
+
* Hypersensitive to the lunar theory: the eccentricity vector amplifies
|
|
427
|
+
* position/velocity differences ~1/e (~18x). Swiss Ephemeris in Moshier
|
|
428
|
+
* mode differs from our DE423 fit by up to ~3 arcmin here; published
|
|
429
|
+
* 'True Lilith' values disagree across software at that scale. */
|
|
430
|
+
function oscApogeeFromState(data, x, y, z, vx, vy, vz, jde, frameJ2000) {
|
|
431
|
+
const mu = GM_EARTH_MOON;
|
|
432
|
+
const r = Math.sqrt(x * x + y * y + z * z);
|
|
433
|
+
const v2 = vx * vx + vy * vy + vz * vz;
|
|
434
|
+
const rv = x * vx + y * vy + z * vz;
|
|
435
|
+
const ex = (v2 * x - rv * vx) / mu - x / r;
|
|
436
|
+
const ey = (v2 * y - rv * vy) / mu - y / r;
|
|
437
|
+
const ez = (v2 * z - rv * vz) / mu - z / r;
|
|
438
|
+
const e = Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
439
|
+
const a = 1.0 / (2.0 / r - v2 / mu);
|
|
440
|
+
const s = (a * (1 + e)) / e;
|
|
441
|
+
let px = -ex * s;
|
|
442
|
+
let py = -ey * s;
|
|
443
|
+
let pz = -ez * s;
|
|
444
|
+
if (frameJ2000)
|
|
445
|
+
[px, py, pz] = eclJ2000ToEclDate([px, py, pz], jde);
|
|
446
|
+
const lon = mod(Math.atan2(py, px) + nutation(data, jde)[0], TWO_PI);
|
|
447
|
+
const lat = Math.atan2(pz, Math.hypot(px, py));
|
|
448
|
+
return [lon, lat, Math.sqrt(px * px + py * py + pz * pz)];
|
|
449
|
+
}
|
|
450
|
+
/** Osculating lunar apogee (True Lilith) from the Chebyshev moon. */
|
|
451
|
+
export function oscApogeePrecise(data, cheb, jde) {
|
|
452
|
+
const [[x, y, z], [vx, vy, vz]] = cheb.xyzVel(jde);
|
|
453
|
+
return oscApogeeFromState(data, x, y, z, vx, vy, vz, jde, true);
|
|
454
|
+
}
|
|
455
|
+
/** Series fallback outside the Chebyshev range (same finite-difference
|
|
456
|
+
* state as the true-node fallback). */
|
|
457
|
+
export function oscApogeeSeries(data, jde) {
|
|
458
|
+
const h = 0.01;
|
|
459
|
+
const xyz = (t) => {
|
|
460
|
+
const [lon, lat, dist] = moonGeometric(data, t);
|
|
461
|
+
return [
|
|
462
|
+
dist * Math.cos(lat) * Math.cos(lon),
|
|
463
|
+
dist * Math.cos(lat) * Math.sin(lon),
|
|
464
|
+
dist * Math.sin(lat),
|
|
465
|
+
];
|
|
466
|
+
};
|
|
467
|
+
const [x0, y0, z0] = xyz(jde - h);
|
|
468
|
+
const [x1, y1, z1] = xyz(jde + h);
|
|
469
|
+
const [x, y, z] = xyz(jde);
|
|
470
|
+
return oscApogeeFromState(data, x, y, z, (x1 - x0) / (2 * h), (y1 - y0) / (2 * h), (z1 - z0) / (2 * h), jde, false);
|
|
471
|
+
}
|
|
472
|
+
/** Constant-element two-body orbit with the same xyz(jde) interface as
|
|
473
|
+
* ChebSeries, so chironApparent takes either. */
|
|
474
|
+
export class KeplerOrbit {
|
|
475
|
+
els;
|
|
476
|
+
epoch;
|
|
477
|
+
constructor(els, epoch) {
|
|
478
|
+
this.els = els;
|
|
479
|
+
this.epoch = epoch;
|
|
480
|
+
}
|
|
481
|
+
xyz(jde) {
|
|
482
|
+
const { a, e, i, node, peri: w, M0, n } = this.els;
|
|
483
|
+
const M = M0 + n * (jde - this.epoch);
|
|
484
|
+
let E = M;
|
|
485
|
+
for (let k = 0; k < 30; k++) {
|
|
486
|
+
E = E - (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E));
|
|
487
|
+
}
|
|
488
|
+
const xv = a * (Math.cos(E) - e);
|
|
489
|
+
const yv = a * Math.sqrt(1 - e * e) * Math.sin(E);
|
|
490
|
+
const cw = Math.cos(w);
|
|
491
|
+
const sw = Math.sin(w);
|
|
492
|
+
const cn = Math.cos(node);
|
|
493
|
+
const sn = Math.sin(node);
|
|
494
|
+
const ci = Math.cos(i);
|
|
495
|
+
const si = Math.sin(i);
|
|
496
|
+
const xp = xv * cw - yv * sw;
|
|
497
|
+
const yp = xv * sw + yv * cw;
|
|
498
|
+
return [xp * cn - yp * sn * ci, xp * sn + yp * cn * ci, yp * si];
|
|
499
|
+
}
|
|
500
|
+
}
|
|
423
501
|
export const EARTH_RADIUS_AU = 6378.14 / 149597870.7;
|
|
424
502
|
const EARTH_FLAT = 0.99664719; // 1 - f, IAU 1976 figure
|
|
425
503
|
/** Diurnal parallax in ecliptic coordinates (Meeus ch. 11/40).
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Engine, BodyId, Zodiac } from "./chart.js";
|
|
2
|
+
export type RiseKind = "rise" | "set" | "mtransit" | "itransit";
|
|
3
|
+
export interface RiseSetOptions {
|
|
4
|
+
altM?: number;
|
|
5
|
+
pressure?: number;
|
|
6
|
+
tempC?: number;
|
|
7
|
+
searchDays?: number;
|
|
8
|
+
/** Rise/set of the disc center instead of the upper limb. */
|
|
9
|
+
discCenter?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Next rise/set/meridian transit (UT JD) after jdStart, or null when the
|
|
12
|
+
* event does not occur in the window (polar day/night). */
|
|
13
|
+
export declare function riseSet(engine: Engine, body: BodyId, jdStart: number, latDeg: number, lonDeg: number, kind?: RiseKind, opts?: RiseSetOptions): number | null;
|
|
14
|
+
/** UT JDs where the body's apparent longitude crosses targetLon (degrees)
|
|
15
|
+
* in [jdStart, jdEnd]. Retrograde bodies can cross a degree three times;
|
|
16
|
+
* every crossing is returned in time order. */
|
|
17
|
+
export declare function crossings(engine: Engine, body: BodyId, targetLon: number, jdStart: number, jdEnd: number, zodiac?: Zodiac, maxHits?: number): number[];
|
|
18
|
+
export type PhaseName = "new" | "first_quarter" | "full" | "last_quarter";
|
|
19
|
+
/** New/first-quarter/full/last-quarter times in [jdStart, jdEnd], sorted. */
|
|
20
|
+
export declare function lunarPhases(engine: Engine, jdStart: number, jdEnd: number, maxHits?: number): Array<[number, PhaseName]>;
|
|
21
|
+
/** Times the body stations (speed crosses zero): [jdUt, direction the body
|
|
22
|
+
* turns]. Sun and Moon never station. Station timing is ill-conditioned:
|
|
23
|
+
* expect minute-level differences between ephemerides. */
|
|
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;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine events -- rise/set/meridian transits, zodiac crossings,
|
|
3
|
+
* lunar phases, stations.
|
|
4
|
+
*
|
|
5
|
+
* Rise/set condition (matches Swiss Ephemeris defaults, calibrated against
|
|
6
|
+
* swe_rise_trans at standard pressure/temperature): the topocentric true
|
|
7
|
+
* altitude of the disc center equals -(R0 + topocentric semidiameter),
|
|
8
|
+
* with R0 = 34.076 arcmin scaled by (pressure/1010)(283/(273+temp)). All
|
|
9
|
+
* searches are bracketed sign changes refined by bisection; speeds come
|
|
10
|
+
* from the same apparent-position pipeline as the chart API, so retrograde
|
|
11
|
+
* loops and multiple crossings are found, not assumed away.
|
|
12
|
+
*/
|
|
13
|
+
import { DEG, mod, jdTT, equatorial, trueObliquity, topocentricEcl, } from "./core.js";
|
|
14
|
+
import { gast } from "./houses.js";
|
|
15
|
+
import { DIAMETER_KM } from "./pheno.js";
|
|
16
|
+
const TWO_PI = 2 * Math.PI;
|
|
17
|
+
const KM_PER_AU = 149597870.7;
|
|
18
|
+
const R0_ARCMIN = 34.076; // horizon refraction at 1010 hPa / 10 C (vs SE)
|
|
19
|
+
function topoAltHa(engine, body, jdUt, latDeg, lonDeg, altM) {
|
|
20
|
+
const jde = jdTT(jdUt);
|
|
21
|
+
let [lon, lat, dist] = engine.ecliptic(body, jde);
|
|
22
|
+
const eps = trueObliquity(engine.data, jde);
|
|
23
|
+
const lst = mod(gast(engine.data, jdUt) + lonDeg * DEG, TWO_PI);
|
|
24
|
+
if (dist !== null) {
|
|
25
|
+
[lon, lat, dist] = topocentricEcl(lon, lat, dist, lst, latDeg * DEG, altM, eps);
|
|
26
|
+
}
|
|
27
|
+
const [ra, dec] = equatorial(lon, lat, eps);
|
|
28
|
+
const ha = mod(lst - ra + Math.PI, TWO_PI) - Math.PI;
|
|
29
|
+
const phi = latDeg * DEG;
|
|
30
|
+
const alt = Math.asin(Math.sin(phi) * Math.sin(dec) + Math.cos(phi) * Math.cos(dec) * Math.cos(ha));
|
|
31
|
+
return [alt, ha, dist];
|
|
32
|
+
}
|
|
33
|
+
function bisect(f, a, b, iters = 45) {
|
|
34
|
+
let fa = f(a);
|
|
35
|
+
for (let i = 0; i < iters; i++) {
|
|
36
|
+
const m = (a + b) / 2;
|
|
37
|
+
if (fa * f(m) <= 0) {
|
|
38
|
+
b = m;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
a = m;
|
|
42
|
+
fa = f(a);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return (a + b) / 2;
|
|
46
|
+
}
|
|
47
|
+
/** Next rise/set/meridian transit (UT JD) after jdStart, or null when the
|
|
48
|
+
* event does not occur in the window (polar day/night). */
|
|
49
|
+
export function riseSet(engine, body, jdStart, latDeg, lonDeg, kind = "rise", opts = {}) {
|
|
50
|
+
const altM = opts.altM ?? 0.0;
|
|
51
|
+
const pressure = opts.pressure ?? 1013.25;
|
|
52
|
+
const tempC = opts.tempC ?? 15.0;
|
|
53
|
+
const searchDays = opts.searchDays ?? 2.0;
|
|
54
|
+
const scale = (pressure / 1010.0) * (283.0 / (273.0 + tempC));
|
|
55
|
+
if (kind === "mtransit" || kind === "itransit") {
|
|
56
|
+
const target = kind === "mtransit" ? 0.0 : Math.PI;
|
|
57
|
+
const g = (t) => {
|
|
58
|
+
const [, ha] = topoAltHa(engine, body, t, latDeg, lonDeg, altM);
|
|
59
|
+
return mod(ha - target + Math.PI, TWO_PI) - Math.PI;
|
|
60
|
+
};
|
|
61
|
+
const step = 1.0 / 48;
|
|
62
|
+
let prev = g(jdStart);
|
|
63
|
+
for (let t = jdStart + step; t <= jdStart + searchDays; t += step) {
|
|
64
|
+
const cur = g(t);
|
|
65
|
+
if (prev * cur < 0 && Math.abs(cur - prev) < Math.PI) {
|
|
66
|
+
return bisect(g, t - step, t);
|
|
67
|
+
}
|
|
68
|
+
prev = cur;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const f = (t) => {
|
|
73
|
+
const [alt, , dist] = topoAltHa(engine, body, t, latDeg, lonDeg, altM);
|
|
74
|
+
let sd = 0.0;
|
|
75
|
+
const diam = DIAMETER_KM[body];
|
|
76
|
+
if (!opts.discCenter && diam !== undefined && dist !== null) {
|
|
77
|
+
sd = Math.asin(diam / (2 * dist * KM_PER_AU));
|
|
78
|
+
}
|
|
79
|
+
const h0 = -((R0_ARCMIN / 60.0) * scale * DEG + sd);
|
|
80
|
+
return alt - h0;
|
|
81
|
+
};
|
|
82
|
+
const step = 1.0 / 48; // 30 min: well under the fastest crossing scale
|
|
83
|
+
let prev = f(jdStart);
|
|
84
|
+
for (let t = jdStart + step; t <= jdStart + searchDays; t += step) {
|
|
85
|
+
const cur = f(t);
|
|
86
|
+
if ((kind === "rise" && prev < 0 && cur >= 0)
|
|
87
|
+
|| (kind === "set" && prev > 0 && cur <= 0)) {
|
|
88
|
+
return bisect(f, t - step, t);
|
|
89
|
+
}
|
|
90
|
+
prev = cur;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** UT JDs where the body's apparent longitude crosses targetLon (degrees)
|
|
95
|
+
* in [jdStart, jdEnd]. Retrograde bodies can cross a degree three times;
|
|
96
|
+
* every crossing is returned in time order. */
|
|
97
|
+
export function crossings(engine, body, targetLon, jdStart, jdEnd, zodiac = "tropical", maxHits = 60) {
|
|
98
|
+
const f = (t) => mod(engine.longitude(body, t, { zodiac }) - targetLon + 180, 360) - 180;
|
|
99
|
+
const fast = body === "moon" || body === "mean_node"
|
|
100
|
+
|| body === "true_node" || body === "mean_lilith" || body === "true_lilith";
|
|
101
|
+
const step = fast ? 0.25 : 1.0;
|
|
102
|
+
const out = [];
|
|
103
|
+
let prev = f(jdStart);
|
|
104
|
+
for (let t = jdStart + step; t <= jdEnd && out.length < maxHits; t += step) {
|
|
105
|
+
const cur = f(t);
|
|
106
|
+
if (prev * cur < 0 && Math.abs(cur - prev) < 180) {
|
|
107
|
+
out.push(bisect(f, t - step, t));
|
|
108
|
+
}
|
|
109
|
+
prev = cur;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/** New/first-quarter/full/last-quarter times in [jdStart, jdEnd], sorted. */
|
|
114
|
+
export function lunarPhases(engine, jdStart, jdEnd, maxHits = 60) {
|
|
115
|
+
const elong = (t) => mod(engine.longitude("moon", t) - engine.longitude("sun", t), 360);
|
|
116
|
+
const names = [
|
|
117
|
+
[0, "new"], [90, "first_quarter"], [180, "full"], [270, "last_quarter"],
|
|
118
|
+
];
|
|
119
|
+
const out = [];
|
|
120
|
+
const step = 0.25;
|
|
121
|
+
for (const [angle, name] of names) {
|
|
122
|
+
const f = (t) => mod(elong(t) - angle + 180, 360) - 180;
|
|
123
|
+
let prev = f(jdStart);
|
|
124
|
+
for (let t = jdStart + step; t <= jdEnd && out.length < maxHits; t += step) {
|
|
125
|
+
const cur = f(t);
|
|
126
|
+
if (prev * cur < 0 && Math.abs(cur - prev) < 180) {
|
|
127
|
+
out.push([bisect(f, t - step, t), name]);
|
|
128
|
+
}
|
|
129
|
+
prev = cur;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
out.sort((a, b) => a[0] - b[0]);
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
/** Times the body stations (speed crosses zero): [jdUt, direction the body
|
|
136
|
+
* turns]. Sun and Moon never station. Station timing is ill-conditioned:
|
|
137
|
+
* expect minute-level differences between ephemerides. */
|
|
138
|
+
export function stations(engine, body, jdStart, jdEnd, maxHits = 30) {
|
|
139
|
+
const h = 0.25;
|
|
140
|
+
const speed = (t) => {
|
|
141
|
+
const l0 = engine.longitude(body, t - h);
|
|
142
|
+
const l1 = engine.longitude(body, t + h);
|
|
143
|
+
return (mod(l1 - l0 + 540, 360) - 180) / (2 * h);
|
|
144
|
+
};
|
|
145
|
+
const step = 2.0;
|
|
146
|
+
const out = [];
|
|
147
|
+
let prev = speed(jdStart);
|
|
148
|
+
for (let t = jdStart + step; t <= jdEnd && out.length < maxHits; t += step) {
|
|
149
|
+
const cur = speed(t);
|
|
150
|
+
if (prev * cur < 0) {
|
|
151
|
+
out.push([bisect(speed, t - step, t), prev > 0 ? "retrograde" : "direct"]);
|
|
152
|
+
}
|
|
153
|
+
prev = cur;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
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
|
@@ -7,8 +7,18 @@ const PLANETS = ["mercury", "venus", "earth", "mars", "jupiter", "saturn",
|
|
|
7
7
|
export function loadNodeData(dir, level = "embedded", moonTier = "full") {
|
|
8
8
|
const j = (name) => JSON.parse(readFileSync(join(dir, name), "utf8"));
|
|
9
9
|
const vsop = {};
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// The npm package ships the embedded and micro VSOP tiers; full/high live
|
|
11
|
+
// in the repo. Fall back per planet so "full" against the published
|
|
12
|
+
// tarball loads instead of throwing ENOENT.
|
|
13
|
+
for (const p of PLANETS) {
|
|
14
|
+
const tiers = level === "embedded" || level === "micro" ? [level] : [level, "embedded"];
|
|
15
|
+
const found = tiers.find((t) => existsSync(join(dir, `vsop87d_${p}.${t}.json`)));
|
|
16
|
+
if (!found) {
|
|
17
|
+
throw new Error(`no VSOP87D data for ${p} in ${dir} (tried ${tiers.join(", ")}); `
|
|
18
|
+
+ "the full/high tiers live in the caelus repo, not the npm package");
|
|
19
|
+
}
|
|
20
|
+
vsop[p] = j(`vsop87d_${p}.${found}.json`);
|
|
21
|
+
}
|
|
12
22
|
const data = {
|
|
13
23
|
vsop,
|
|
14
24
|
nutation: j("nutation_iau1980.json"),
|
|
@@ -18,6 +28,18 @@ export function loadNodeData(dir, level = "embedded", moonTier = "full") {
|
|
|
18
28
|
const chironPath = join(dir, "chiron_cheb.json");
|
|
19
29
|
if (existsSync(chironPath))
|
|
20
30
|
data.chiron = j("chiron_cheb.json");
|
|
31
|
+
if (existsSync(join(dir, "uranian_kepler.json"))) {
|
|
32
|
+
data.keplerPack = j("uranian_kepler.json");
|
|
33
|
+
}
|
|
34
|
+
if (existsSync(join(dir, "fixed_stars.json"))) {
|
|
35
|
+
data.fixedStars = j("fixed_stars.json");
|
|
36
|
+
}
|
|
37
|
+
// asteroid packs (Horizons fits): loaded when present, ~380 KB total
|
|
38
|
+
for (const b of ["ceres", "pallas", "juno", "vesta", "pholus"]) {
|
|
39
|
+
if (existsSync(join(dir, `${b}_cheb.json`))) {
|
|
40
|
+
(data.chebPacks ??= {})[b] = j(`${b}_cheb.json`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
21
43
|
if (moonTier !== "none") {
|
|
22
44
|
// The npm package ships only the embedded tier (1920-2080); the full
|
|
23
45
|
// tier (1850-2150, 3.1 MB, same precision) lives in the repo. Fall back
|
|
@@ -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
|
+
}
|