caelus 0.16.0 → 0.18.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 +15 -15
- package/dist/src/anchored.d.ts +49 -0
- package/dist/src/anchored.js +40 -0
- package/dist/src/astrocartography.js +6 -1
- package/dist/src/brief.d.ts +90 -0
- package/dist/src/brief.js +85 -0
- package/dist/src/chart.d.ts +5 -0
- package/dist/src/chart.js +20 -4
- package/dist/src/counterfactual.d.ts +82 -0
- package/dist/src/counterfactual.js +105 -0
- package/dist/src/eclipses.d.ts +103 -0
- package/dist/src/eclipses.js +260 -2
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +6 -0
- package/dist/src/interpret.d.ts +156 -0
- package/dist/src/interpret.js +179 -0
- package/dist/src/interpretation.d.ts +137 -0
- package/dist/src/interpretation.js +250 -0
- package/dist/src/node-loader.js +5 -2
- package/dist/src/parans.d.ts +28 -0
- package/dist/src/parans.js +84 -0
- package/dist/src/provenance.d.ts +135 -0
- package/dist/src/provenance.js +159 -0
- package/package.json +1 -1
package/dist/src/eclipses.d.ts
CHANGED
|
@@ -38,3 +38,106 @@ export declare function lunarEclipses(engine: Engine, jdStart: number, jdEnd: nu
|
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
40
|
export declare function solarEclipses(engine: Engine, jdStart: number, jdEnd: number): SolarEclipse[];
|
|
41
|
+
/** Geographic point on the Earth's surface (geodetic latitude, east longitude). */
|
|
42
|
+
export interface GeoPoint {
|
|
43
|
+
/** Geodetic latitude in degrees, north positive. */
|
|
44
|
+
lat: number;
|
|
45
|
+
/** Longitude in degrees, east positive, in (-180, 180]. */
|
|
46
|
+
lonEast: number;
|
|
47
|
+
}
|
|
48
|
+
/** Local circumstances of a solar eclipse seen from one place. */
|
|
49
|
+
export interface SolarLocal {
|
|
50
|
+
/** What the observer sees at maximum: `"none"` when no part of the Sun is
|
|
51
|
+
* covered from this place. */
|
|
52
|
+
type: "total" | "annular" | "partial" | "none";
|
|
53
|
+
/** Eclipse magnitude: fraction of the Sun's *diameter* covered at maximum
|
|
54
|
+
* (can exceed 1 in totality). 0 when `type` is `"none"`. */
|
|
55
|
+
magnitude: number;
|
|
56
|
+
/** Obscuration: fraction of the Sun's *area* covered at maximum, in [0, 1]. */
|
|
57
|
+
obscuration: number;
|
|
58
|
+
/** Time of maximum eclipse at this place (JD UT), or `null` when unseen. */
|
|
59
|
+
maxTime: number | null;
|
|
60
|
+
/** First contact (partial begins), JD UT, or `null` when unseen. */
|
|
61
|
+
c1: number | null;
|
|
62
|
+
/** Second contact (totality/annularity begins), JD UT, or `null`. */
|
|
63
|
+
c2: number | null;
|
|
64
|
+
/** Third contact (totality/annularity ends), JD UT, or `null`. */
|
|
65
|
+
c3: number | null;
|
|
66
|
+
/** Fourth contact (partial ends), JD UT, or `null` when unseen. */
|
|
67
|
+
c4: number | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Sub-shadow geographic point where the eclipse axis meets the Earth at a
|
|
71
|
+
* Julian Day (UT): the centre line of totality/annularity at that instant.
|
|
72
|
+
* Sample it across the eclipse (e.g. between the {@link SolarEclipse} `begin`
|
|
73
|
+
* and `end`) to draw the ground track.
|
|
74
|
+
*
|
|
75
|
+
* @param engine The engine used to evaluate positions.
|
|
76
|
+
* @param jd Instant to evaluate, Julian Day (UT) -- typically a
|
|
77
|
+
* {@link SolarEclipse.tMax} for the point of greatest eclipse.
|
|
78
|
+
* @returns The {@link GeoPoint} on the IAU 1976 ellipsoid, or `null` when the
|
|
79
|
+
* axis misses the Earth (only a partial eclipse exists anywhere then).
|
|
80
|
+
*/
|
|
81
|
+
export declare function solarEclipseWhere(engine: Engine, jd: number): GeoPoint | null;
|
|
82
|
+
/**
|
|
83
|
+
* Local circumstances of a solar eclipse as seen from one place: contact
|
|
84
|
+
* times, magnitude, and obscuration. Topocentric Sun and Moon disks, so it
|
|
85
|
+
* accounts for lunar parallax (which is what makes the same eclipse total in
|
|
86
|
+
* one town and partial in the next).
|
|
87
|
+
*
|
|
88
|
+
* @param engine The engine used to evaluate positions.
|
|
89
|
+
* @param jd A time near the eclipse, JD (UT) -- typically a
|
|
90
|
+
* {@link SolarEclipse.tMax}; the local maximum is found within a few hours.
|
|
91
|
+
* @param latDeg Observer geodetic latitude in degrees (north positive).
|
|
92
|
+
* @param lonEastDeg Observer longitude in degrees (east positive).
|
|
93
|
+
* @param altM Observer height above the ellipsoid in metres (default 0).
|
|
94
|
+
* @returns {@link SolarLocal}. `type` is `"none"` when the Sun is not eclipsed
|
|
95
|
+
* from this place at all; `c2`/`c3` are `null` outside totality/annularity.
|
|
96
|
+
*/
|
|
97
|
+
export declare function solarEclipseLocal(engine: Engine, jd: number, latDeg: number, lonEastDeg: number, altM?: number): SolarLocal;
|
|
98
|
+
/** The umbra/antumbra path of a solar eclipse at one instant. */
|
|
99
|
+
export interface EclipsePath {
|
|
100
|
+
/** Central line point (greatest coverage) at this instant. */
|
|
101
|
+
center: GeoPoint;
|
|
102
|
+
/** Northern edge of totality/annularity, or `null` if it runs off the Earth. */
|
|
103
|
+
north: GeoPoint | null;
|
|
104
|
+
/** Southern edge, or `null` if it runs off the Earth. */
|
|
105
|
+
south: GeoPoint | null;
|
|
106
|
+
/** Full path width (km) between the limits, or `null` when a limit is missing. */
|
|
107
|
+
widthKm: number | null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Ground path of a solar eclipse at a Julian Day (UT): the central point and
|
|
111
|
+
* the north/south limits of totality (or annularity), with the path width.
|
|
112
|
+
* Marches perpendicular to the shadow's ground track out to the umbra edge --
|
|
113
|
+
* where the Moon just fully covers (or is covered by) the Sun. Sample across
|
|
114
|
+
* the eclipse to trace the full path of totality.
|
|
115
|
+
*
|
|
116
|
+
* @param engine The engine used to evaluate positions.
|
|
117
|
+
* @param jd Instant to evaluate, Julian Day (UT) -- typically a
|
|
118
|
+
* {@link SolarEclipse.tMax} for the path at greatest eclipse.
|
|
119
|
+
* @returns The {@link EclipsePath}, or `null` when no central eclipse exists
|
|
120
|
+
* then (only a partial eclipse, or the axis misses the Earth).
|
|
121
|
+
*/
|
|
122
|
+
export declare function solarEclipseLimits(engine: Engine, jd: number): EclipsePath | null;
|
|
123
|
+
/** Whether a lunar eclipse is up at a place, and how high the Moon stands. */
|
|
124
|
+
export interface LunarLocal {
|
|
125
|
+
/** Moon's true altitude in degrees at the given instant (negative = below
|
|
126
|
+
* the horizon). */
|
|
127
|
+
altitude: number;
|
|
128
|
+
/** Whether the Moon is above the horizon (the eclipse is visible there). */
|
|
129
|
+
visible: boolean;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Local visibility of a lunar eclipse: a lunar eclipse happens at the same
|
|
133
|
+
* instant for the whole Earth, so "local circumstances" is simply whether the
|
|
134
|
+
* Moon is up. Pass a contact time (e.g. {@link LunarEclipse.tMax} or a phase
|
|
135
|
+
* boundary) to learn whether that phase is visible from a place.
|
|
136
|
+
*
|
|
137
|
+
* @param engine The engine used to evaluate positions.
|
|
138
|
+
* @param jd Instant to evaluate, Julian Day (UT).
|
|
139
|
+
* @param latDeg Observer latitude in degrees (north positive).
|
|
140
|
+
* @param lonEastDeg Observer longitude in degrees (east positive).
|
|
141
|
+
* @returns {@link LunarLocal} with the Moon's altitude and a visibility flag.
|
|
142
|
+
*/
|
|
143
|
+
export declare function lunarEclipseLocal(engine: Engine, jd: number, latDeg: number, lonEastDeg: number): LunarLocal;
|
package/dist/src/eclipses.js
CHANGED
|
@@ -11,9 +11,18 @@
|
|
|
11
11
|
* Sun-Moon axis to the geocenter in Earth radii; the umbral cone's reach
|
|
12
12
|
* at the surface separates total from annular, a sign change along the
|
|
13
13
|
* track marks hybrids. Types match Swiss Ephemeris exactly over decades.
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* Solar (where): the same shadow axis intersected with the IAU 1976 Earth
|
|
16
|
+
* ellipsoid gives the sub-shadow geographic point -- the centre line of
|
|
17
|
+
* totality/annularity at an instant; sampled across the eclipse it draws the
|
|
18
|
+
* ground track. Solar (local): topocentric Sun/Moon disks at an observer give
|
|
19
|
+
* the contact times, magnitude, and obscuration as seen from that point. With
|
|
20
|
+
* a ~2.5" Moon these land within seconds of time and a few km of track: right
|
|
21
|
+
* for charts, not for eclipse-chaser path maps.
|
|
15
22
|
*/
|
|
16
|
-
import { ARCSEC, jdTT, mod } from "./core.js";
|
|
23
|
+
import { ARCSEC, DEG, jdTT, mod, trueObliquity, equatorial, topocentricEcl } from "./core.js";
|
|
24
|
+
import { gast } from "./houses.js";
|
|
25
|
+
import { azAlt } from "./pheno.js";
|
|
17
26
|
const KM_PER_AU = 149597870.7;
|
|
18
27
|
const R_EARTH = 6378.14;
|
|
19
28
|
const R_SUN = 696000.0;
|
|
@@ -177,3 +186,252 @@ export function solarEclipses(engine, jdStart, jdEnd) {
|
|
|
177
186
|
}
|
|
178
187
|
return out;
|
|
179
188
|
}
|
|
189
|
+
// ---------------------------------------------------------------- where + local
|
|
190
|
+
const EARTH_FLAT = 0.99664719; // 1 - f, IAU 1976 figure (b/a)
|
|
191
|
+
const EARTH_FLAT2 = EARTH_FLAT * EARTH_FLAT; // (b/a)^2 = 1 - e^2
|
|
192
|
+
/** Geocentric *equatorial* Cartesian (km) of the Sun and Moon, plus obliquity. */
|
|
193
|
+
function sunMoonEq(engine, jde) {
|
|
194
|
+
const eps = trueObliquity(engine.data, jde);
|
|
195
|
+
const vec = (body) => {
|
|
196
|
+
const [lon, lat, dist] = engine.ecliptic(body, jde);
|
|
197
|
+
const [ra, dec] = equatorial(lon, lat, eps);
|
|
198
|
+
const r = dist * KM_PER_AU;
|
|
199
|
+
return [
|
|
200
|
+
r * Math.cos(dec) * Math.cos(ra),
|
|
201
|
+
r * Math.cos(dec) * Math.sin(ra),
|
|
202
|
+
r * Math.sin(dec),
|
|
203
|
+
];
|
|
204
|
+
};
|
|
205
|
+
return { S: vec("sun"), M: vec("moon") };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Sub-shadow geographic point where the eclipse axis meets the Earth at a
|
|
209
|
+
* Julian Day (UT): the centre line of totality/annularity at that instant.
|
|
210
|
+
* Sample it across the eclipse (e.g. between the {@link SolarEclipse} `begin`
|
|
211
|
+
* and `end`) to draw the ground track.
|
|
212
|
+
*
|
|
213
|
+
* @param engine The engine used to evaluate positions.
|
|
214
|
+
* @param jd Instant to evaluate, Julian Day (UT) -- typically a
|
|
215
|
+
* {@link SolarEclipse.tMax} for the point of greatest eclipse.
|
|
216
|
+
* @returns The {@link GeoPoint} on the IAU 1976 ellipsoid, or `null` when the
|
|
217
|
+
* axis misses the Earth (only a partial eclipse exists anywhere then).
|
|
218
|
+
*/
|
|
219
|
+
export function solarEclipseWhere(engine, jd) {
|
|
220
|
+
const jde = jdTT(jd);
|
|
221
|
+
const { S, M } = sunMoonEq(engine, jde);
|
|
222
|
+
const SM = [M[0] - S[0], M[1] - S[1], M[2] - S[2]];
|
|
223
|
+
const smn = Math.hypot(SM[0], SM[1], SM[2]);
|
|
224
|
+
const d = SM.map((c) => c / smn); // travels Sun -> Moon -> Earth
|
|
225
|
+
// Intersect the line M + s*d with the ellipsoid by scaling z by 1/flat,
|
|
226
|
+
// which maps the ellipsoid to a sphere of radius R_EARTH.
|
|
227
|
+
const Mz = [M[0], M[1], M[2] / EARTH_FLAT];
|
|
228
|
+
const dz = [d[0], d[1], d[2] / EARTH_FLAT];
|
|
229
|
+
const a = dz[0] ** 2 + dz[1] ** 2 + dz[2] ** 2;
|
|
230
|
+
const b = 2 * (Mz[0] * dz[0] + Mz[1] * dz[1] + Mz[2] * dz[2]);
|
|
231
|
+
const c = Mz[0] ** 2 + Mz[1] ** 2 + Mz[2] ** 2 - R_EARTH ** 2;
|
|
232
|
+
const disc = b * b - 4 * a * c;
|
|
233
|
+
if (disc < 0)
|
|
234
|
+
return null;
|
|
235
|
+
const s = (-b - Math.sqrt(disc)) / (2 * a); // near side, facing the Moon
|
|
236
|
+
const P = [M[0] + s * d[0], M[1] + s * d[1], M[2] + s * d[2]];
|
|
237
|
+
const rho = Math.hypot(P[0], P[1]);
|
|
238
|
+
const lat = Math.atan2(P[2], EARTH_FLAT2 * rho); // geocentric -> geodetic
|
|
239
|
+
const ra = Math.atan2(P[1], P[0]);
|
|
240
|
+
const lonEast = mod(ra - gast(engine.data, jd) + Math.PI, 2 * Math.PI) - Math.PI;
|
|
241
|
+
return { lat: lat / DEG, lonEast: lonEast / DEG };
|
|
242
|
+
}
|
|
243
|
+
/** Topocentric Sun/Moon angular separation and disk radii (rad) at a place. */
|
|
244
|
+
function topoCircs(engine, jd, latDeg, lonEastDeg, altM) {
|
|
245
|
+
const jde = jdTT(jd);
|
|
246
|
+
const eps = trueObliquity(engine.data, jde);
|
|
247
|
+
const lst = mod(gast(engine.data, jd) + lonEastDeg * DEG, 2 * Math.PI);
|
|
248
|
+
const topo = (body) => {
|
|
249
|
+
const [lon, lat, dist] = engine.ecliptic(body, jde);
|
|
250
|
+
return topocentricEcl(lon, lat, dist, lst, latDeg * DEG, altM, eps);
|
|
251
|
+
};
|
|
252
|
+
const [slon, slat, sdist] = topo("sun");
|
|
253
|
+
const [mlon, mlat, mdist] = topo("moon");
|
|
254
|
+
const cosSep = Math.sin(slat) * Math.sin(mlat)
|
|
255
|
+
+ Math.cos(slat) * Math.cos(mlat) * Math.cos(slon - mlon);
|
|
256
|
+
return {
|
|
257
|
+
sep: Math.acos(Math.max(-1, Math.min(1, cosSep))),
|
|
258
|
+
sS: Math.asin(R_SUN / (sdist * KM_PER_AU)),
|
|
259
|
+
sM: Math.asin(R_MOON / (mdist * KM_PER_AU)),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/** Area where two disks (radii r1, r2, centre distance d) overlap. */
|
|
263
|
+
function lensArea(d, r1, r2) {
|
|
264
|
+
if (d >= r1 + r2)
|
|
265
|
+
return 0;
|
|
266
|
+
if (d <= Math.abs(r1 - r2))
|
|
267
|
+
return Math.PI * Math.min(r1, r2) ** 2;
|
|
268
|
+
const a1 = Math.acos((d * d + r1 * r1 - r2 * r2) / (2 * d * r1));
|
|
269
|
+
const a2 = Math.acos((d * d + r2 * r2 - r1 * r1) / (2 * d * r2));
|
|
270
|
+
return r1 * r1 * (a1 - Math.sin(2 * a1) / 2) + r2 * r2 * (a2 - Math.sin(2 * a2) / 2);
|
|
271
|
+
}
|
|
272
|
+
/** Step out from `tMax` (where g < 0) until `g` changes sign, then bisect. */
|
|
273
|
+
function contact(g, tMax, dir) {
|
|
274
|
+
const step = 0.003; // ~4.3 min
|
|
275
|
+
let prev = tMax;
|
|
276
|
+
let fprev = g(tMax);
|
|
277
|
+
for (let i = 1; i <= 120; i++) { // search up to ~8.6 h either side
|
|
278
|
+
const t = tMax + dir * i * step;
|
|
279
|
+
const f = g(t);
|
|
280
|
+
if (fprev * f <= 0)
|
|
281
|
+
return bisect(g, Math.min(prev, t), Math.max(prev, t));
|
|
282
|
+
prev = t;
|
|
283
|
+
fprev = f;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Local circumstances of a solar eclipse as seen from one place: contact
|
|
289
|
+
* times, magnitude, and obscuration. Topocentric Sun and Moon disks, so it
|
|
290
|
+
* accounts for lunar parallax (which is what makes the same eclipse total in
|
|
291
|
+
* one town and partial in the next).
|
|
292
|
+
*
|
|
293
|
+
* @param engine The engine used to evaluate positions.
|
|
294
|
+
* @param jd A time near the eclipse, JD (UT) -- typically a
|
|
295
|
+
* {@link SolarEclipse.tMax}; the local maximum is found within a few hours.
|
|
296
|
+
* @param latDeg Observer geodetic latitude in degrees (north positive).
|
|
297
|
+
* @param lonEastDeg Observer longitude in degrees (east positive).
|
|
298
|
+
* @param altM Observer height above the ellipsoid in metres (default 0).
|
|
299
|
+
* @returns {@link SolarLocal}. `type` is `"none"` when the Sun is not eclipsed
|
|
300
|
+
* from this place at all; `c2`/`c3` are `null` outside totality/annularity.
|
|
301
|
+
*/
|
|
302
|
+
export function solarEclipseLocal(engine, jd, latDeg, lonEastDeg, altM = 0) {
|
|
303
|
+
const sepAt = (t) => topoCircs(engine, t, latDeg, lonEastDeg, altM).sep;
|
|
304
|
+
const tMax = minimize(sepAt, jd - 0.2, jd + 0.2);
|
|
305
|
+
const { sep, sS, sM } = topoCircs(engine, tMax, latDeg, lonEastDeg, altM);
|
|
306
|
+
const none = {
|
|
307
|
+
type: "none", magnitude: 0, obscuration: 0,
|
|
308
|
+
maxTime: null, c1: null, c2: null, c3: null, c4: null,
|
|
309
|
+
};
|
|
310
|
+
if (sep >= sS + sM)
|
|
311
|
+
return none;
|
|
312
|
+
const type = sep <= sM - sS ? "total" : sep <= sS - sM ? "annular" : "partial";
|
|
313
|
+
const gOuter = (t) => {
|
|
314
|
+
const c = topoCircs(engine, t, latDeg, lonEastDeg, altM);
|
|
315
|
+
return c.sep - (c.sS + c.sM);
|
|
316
|
+
};
|
|
317
|
+
let c2 = null;
|
|
318
|
+
let c3 = null;
|
|
319
|
+
if (type === "total" || type === "annular") {
|
|
320
|
+
const gInner = (t) => {
|
|
321
|
+
const c = topoCircs(engine, t, latDeg, lonEastDeg, altM);
|
|
322
|
+
return c.sep - Math.abs(c.sM - c.sS);
|
|
323
|
+
};
|
|
324
|
+
c2 = contact(gInner, tMax, -1);
|
|
325
|
+
c3 = contact(gInner, tMax, 1);
|
|
326
|
+
}
|
|
327
|
+
// Magnitude = fraction of the Sun's diameter covered. In the partial regime
|
|
328
|
+
// one Moon edge is inside the Sun's disk; once central (annular or total) it
|
|
329
|
+
// is the Moon/Sun diameter ratio -- < 1 in annularity, > 1 in totality.
|
|
330
|
+
return {
|
|
331
|
+
type,
|
|
332
|
+
magnitude: sep <= Math.abs(sM - sS) ? sM / sS : (sS + sM - sep) / (2 * sS),
|
|
333
|
+
obscuration: lensArea(sep, sS, sM) / (Math.PI * sS * sS),
|
|
334
|
+
maxTime: tMax,
|
|
335
|
+
c1: contact(gOuter, tMax, -1),
|
|
336
|
+
c2, c3,
|
|
337
|
+
c4: contact(gOuter, tMax, 1),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const R_MEAN = 6371.0; // mean Earth radius (km) for short surface offsets
|
|
341
|
+
/** Geodetic destination point `distKm` from (lat, lon) along `bearingDeg`. */
|
|
342
|
+
function destPoint(lat, lon, bearingDeg, distKm) {
|
|
343
|
+
const d = distKm / R_MEAN;
|
|
344
|
+
const br = bearingDeg * DEG;
|
|
345
|
+
const p1 = lat * DEG;
|
|
346
|
+
const l1 = lon * DEG;
|
|
347
|
+
const p2 = Math.asin(Math.sin(p1) * Math.cos(d) + Math.cos(p1) * Math.sin(d) * Math.cos(br));
|
|
348
|
+
const l2 = l1 + Math.atan2(Math.sin(br) * Math.sin(d) * Math.cos(p1), Math.cos(d) - Math.sin(p1) * Math.sin(p2));
|
|
349
|
+
return { lat: p2 / DEG, lonEast: mod(l2 / DEG + 540, 360) - 180 };
|
|
350
|
+
}
|
|
351
|
+
/** Initial great-circle bearing (deg) from `a` to `b`. */
|
|
352
|
+
function bearing(a, b) {
|
|
353
|
+
const p1 = a.lat * DEG;
|
|
354
|
+
const p2 = b.lat * DEG;
|
|
355
|
+
const dl = (b.lonEast - a.lonEast) * DEG;
|
|
356
|
+
return mod(Math.atan2(Math.sin(dl) * Math.cos(p2), Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(dl)) / DEG, 360);
|
|
357
|
+
}
|
|
358
|
+
/** Great-circle distance (km) between two geographic points. */
|
|
359
|
+
function greatCircleKm(a, b) {
|
|
360
|
+
const p1 = a.lat * DEG;
|
|
361
|
+
const p2 = b.lat * DEG;
|
|
362
|
+
const dp = (b.lat - a.lat) * DEG;
|
|
363
|
+
const dl = (b.lonEast - a.lonEast) * DEG;
|
|
364
|
+
const h = Math.sin(dp / 2) ** 2 + Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) ** 2;
|
|
365
|
+
return R_MEAN * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Ground path of a solar eclipse at a Julian Day (UT): the central point and
|
|
369
|
+
* the north/south limits of totality (or annularity), with the path width.
|
|
370
|
+
* Marches perpendicular to the shadow's ground track out to the umbra edge --
|
|
371
|
+
* where the Moon just fully covers (or is covered by) the Sun. Sample across
|
|
372
|
+
* the eclipse to trace the full path of totality.
|
|
373
|
+
*
|
|
374
|
+
* @param engine The engine used to evaluate positions.
|
|
375
|
+
* @param jd Instant to evaluate, Julian Day (UT) -- typically a
|
|
376
|
+
* {@link SolarEclipse.tMax} for the path at greatest eclipse.
|
|
377
|
+
* @returns The {@link EclipsePath}, or `null` when no central eclipse exists
|
|
378
|
+
* then (only a partial eclipse, or the axis misses the Earth).
|
|
379
|
+
*/
|
|
380
|
+
export function solarEclipseLimits(engine, jd) {
|
|
381
|
+
const center = solarEclipseWhere(engine, jd);
|
|
382
|
+
if (center === null)
|
|
383
|
+
return null;
|
|
384
|
+
const ahead = solarEclipseWhere(engine, jd + 1 / 86400);
|
|
385
|
+
const track = ahead ? bearing(center, ahead) : 0; // ground-track direction
|
|
386
|
+
// Inside the umbra/antumbra the centres are closer than |radii difference|.
|
|
387
|
+
const edge = (lat, lon) => {
|
|
388
|
+
const c = topoCircs(engine, jd, lat, lon, 0);
|
|
389
|
+
return c.sep - Math.abs(c.sM - c.sS);
|
|
390
|
+
};
|
|
391
|
+
const march = (brg) => {
|
|
392
|
+
let gPrev = edge(center.lat, center.lonEast); // < 0 on the central line
|
|
393
|
+
for (let s = 4; s <= 400; s += 4) {
|
|
394
|
+
const q = destPoint(center.lat, center.lonEast, brg, s);
|
|
395
|
+
if (gPrev * edge(q.lat, q.lonEast) <= 0) {
|
|
396
|
+
let lo = s - 4;
|
|
397
|
+
let hi = s; // edge is between lo (inside) and hi (outside)
|
|
398
|
+
for (let i = 0; i < 40; i++) {
|
|
399
|
+
const mid = (lo + hi) / 2;
|
|
400
|
+
const qm = destPoint(center.lat, center.lonEast, brg, mid);
|
|
401
|
+
if (edge(qm.lat, qm.lonEast) <= 0)
|
|
402
|
+
lo = mid;
|
|
403
|
+
else
|
|
404
|
+
hi = mid;
|
|
405
|
+
}
|
|
406
|
+
return destPoint(center.lat, center.lonEast, brg, (lo + hi) / 2);
|
|
407
|
+
}
|
|
408
|
+
gPrev = edge(q.lat, q.lonEast);
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
};
|
|
412
|
+
const a = march(mod(track - 90, 360));
|
|
413
|
+
const b = march(mod(track + 90, 360));
|
|
414
|
+
// Label by latitude so `north` is always the higher-latitude edge.
|
|
415
|
+
const [north, south] = !a || !b ? [a, b] : a.lat >= b.lat ? [a, b] : [b, a];
|
|
416
|
+
return {
|
|
417
|
+
center, north, south,
|
|
418
|
+
widthKm: north && south ? greatCircleKm(north, south) : null,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Local visibility of a lunar eclipse: a lunar eclipse happens at the same
|
|
423
|
+
* instant for the whole Earth, so "local circumstances" is simply whether the
|
|
424
|
+
* Moon is up. Pass a contact time (e.g. {@link LunarEclipse.tMax} or a phase
|
|
425
|
+
* boundary) to learn whether that phase is visible from a place.
|
|
426
|
+
*
|
|
427
|
+
* @param engine The engine used to evaluate positions.
|
|
428
|
+
* @param jd Instant to evaluate, Julian Day (UT).
|
|
429
|
+
* @param latDeg Observer latitude in degrees (north positive).
|
|
430
|
+
* @param lonEastDeg Observer longitude in degrees (east positive).
|
|
431
|
+
* @returns {@link LunarLocal} with the Moon's altitude and a visibility flag.
|
|
432
|
+
*/
|
|
433
|
+
export function lunarEclipseLocal(engine, jd, latDeg, lonEastDeg) {
|
|
434
|
+
const [mlon, mlat] = engine.ecliptic("moon", jdTT(jd));
|
|
435
|
+
const [, altitude] = azAlt(engine.data, mlon / DEG, mlat / DEG, jd, latDeg, lonEastDeg);
|
|
436
|
+
return { altitude, visible: altitude > 0 };
|
|
437
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -28,5 +28,11 @@ export * from "./ashtottari.js";
|
|
|
28
28
|
export * from "./rajayoga.js";
|
|
29
29
|
export * from "./patterns.js";
|
|
30
30
|
export * from "./signature.js";
|
|
31
|
+
export * from "./interpretation.js";
|
|
32
|
+
export * from "./interpret.js";
|
|
33
|
+
export * from "./brief.js";
|
|
34
|
+
export * from "./provenance.js";
|
|
35
|
+
export * from "./anchored.js";
|
|
36
|
+
export * from "./counterfactual.js";
|
|
31
37
|
export * from "./dignity-score.js";
|
|
32
38
|
export * from "./parans.js";
|
package/dist/src/index.js
CHANGED
|
@@ -28,5 +28,11 @@ export * from "./ashtottari.js";
|
|
|
28
28
|
export * from "./rajayoga.js";
|
|
29
29
|
export * from "./patterns.js";
|
|
30
30
|
export * from "./signature.js";
|
|
31
|
+
export * from "./interpretation.js";
|
|
32
|
+
export * from "./interpret.js";
|
|
33
|
+
export * from "./brief.js";
|
|
34
|
+
export * from "./provenance.js";
|
|
35
|
+
export * from "./anchored.js";
|
|
36
|
+
export * from "./counterfactual.js";
|
|
31
37
|
export * from "./dignity-score.js";
|
|
32
38
|
export * from "./parans.js";
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine interpretation matching + resolver -- the plug for a content layer.
|
|
3
|
+
*
|
|
4
|
+
* The {@link interpretationContext} projection turns a chart into ranked fact
|
|
5
|
+
* atoms; this layer lets a developer plug in *meaning*. A {@link Selector}
|
|
6
|
+
* matches atoms (and reports which ones, so a claim carries its provenance); a
|
|
7
|
+
* {@link Rule} pairs a selector with text; an {@link InterpretationSource}
|
|
8
|
+
* bundles rules (a tradition, a house style, a third-party corpus). The engine
|
|
9
|
+
* ships the contract and the resolver, never the content: {@link interpret}
|
|
10
|
+
* runs sources against a context and returns a ranked {@link Reading}, each
|
|
11
|
+
* entry tagged with the atom ids it rests on.
|
|
12
|
+
*
|
|
13
|
+
* Selectors read the projection directly, so they express the whole fact model
|
|
14
|
+
* -- house, dignity, pattern membership, signature dominance, aspect phase and
|
|
15
|
+
* strength -- which the geometric, time-only `query` predicates cannot.
|
|
16
|
+
*/
|
|
17
|
+
import type { FactAtom, InterpretationContext } from "./interpretation.js";
|
|
18
|
+
/** The result of a {@link Selector}: did it match, and on which atoms. */
|
|
19
|
+
export interface Match {
|
|
20
|
+
matched: boolean;
|
|
21
|
+
/** The atoms that satisfied the selector (the provenance). Empty for a
|
|
22
|
+
* satisfied absence test ({@link matchNone}). */
|
|
23
|
+
atoms: FactAtom[];
|
|
24
|
+
}
|
|
25
|
+
/** Tests a whole {@link InterpretationContext} and reports the matching atoms. */
|
|
26
|
+
export type Selector = (ctx: InterpretationContext) => Match;
|
|
27
|
+
/** Matches placement atoms by any subset of body / sign / house / retrograde /
|
|
28
|
+
* a held dignity. */
|
|
29
|
+
export declare function hasPlacement(filter?: {
|
|
30
|
+
body?: string;
|
|
31
|
+
sign?: string;
|
|
32
|
+
house?: number;
|
|
33
|
+
retrograde?: boolean;
|
|
34
|
+
dignity?: string;
|
|
35
|
+
}): Selector;
|
|
36
|
+
/** Matches aspect atoms. `between` is an unordered pair; `minStrength` filters
|
|
37
|
+
* loose aspects; `phase` filters applying/separating. */
|
|
38
|
+
export declare function hasAspect(filter?: {
|
|
39
|
+
a?: string;
|
|
40
|
+
b?: string;
|
|
41
|
+
between?: [string, string];
|
|
42
|
+
aspect?: string;
|
|
43
|
+
phase?: string;
|
|
44
|
+
minStrength?: number;
|
|
45
|
+
}): Selector;
|
|
46
|
+
/** Matches configuration atoms by kind and/or a participating body. */
|
|
47
|
+
export declare function hasPattern(filter?: {
|
|
48
|
+
kind?: string;
|
|
49
|
+
body?: string;
|
|
50
|
+
}): Selector;
|
|
51
|
+
/** Matches a structural-signature facet, e.g. `("element", "fire")`. */
|
|
52
|
+
export declare function hasSignature(facet: string, value?: string): Selector;
|
|
53
|
+
/** Matches an angle atom by which angle and/or its sign. */
|
|
54
|
+
export declare function hasAngle(angle: string, sign?: string): Selector;
|
|
55
|
+
/** Matches dispositor atoms by body, its dispositor, and/or the final flag
|
|
56
|
+
* (a body in its own domicile that terminates a dispositor chain). */
|
|
57
|
+
export declare function hasDispositor(filter?: {
|
|
58
|
+
body?: string;
|
|
59
|
+
dispositor?: string;
|
|
60
|
+
final?: boolean;
|
|
61
|
+
}): Selector;
|
|
62
|
+
/** Matches a mutual reception, optionally involving a given body. */
|
|
63
|
+
export declare function hasReception(filter?: {
|
|
64
|
+
body?: string;
|
|
65
|
+
}): Selector;
|
|
66
|
+
/** Matches only when every selector matches; returns the union of their atoms. */
|
|
67
|
+
export declare function matchAll(...sels: Selector[]): Selector;
|
|
68
|
+
/** Matches when any selector matches; returns the atoms from those that did. */
|
|
69
|
+
export declare function matchAny(...sels: Selector[]): Selector;
|
|
70
|
+
/** Matches when the selector does NOT match (an absence test); no atoms. */
|
|
71
|
+
export declare function matchNone(sel: Selector): Selector;
|
|
72
|
+
/** One interpretation: a condition and the text it licenses. */
|
|
73
|
+
export interface Rule {
|
|
74
|
+
/** Stable id, unique within its source. */
|
|
75
|
+
id: string;
|
|
76
|
+
/** The condition over the fact projection. */
|
|
77
|
+
when: Selector;
|
|
78
|
+
/** The interpretation text, or a function of the match for templating. */
|
|
79
|
+
text: string | ((match: Match, ctx: InterpretationContext) => string);
|
|
80
|
+
/** Multiplies the matched atoms' salience when ranking (default 1). */
|
|
81
|
+
weight?: number;
|
|
82
|
+
/** Free-form labels (theme, polarity, ...) carried through to the entry. */
|
|
83
|
+
tags?: string[];
|
|
84
|
+
}
|
|
85
|
+
/** A pluggable corpus of rules: a tradition, a house style, a third party. */
|
|
86
|
+
export interface InterpretationSource {
|
|
87
|
+
id: string;
|
|
88
|
+
version: string;
|
|
89
|
+
rules: Rule[];
|
|
90
|
+
}
|
|
91
|
+
/** One licensed statement in a {@link Reading}, with its provenance. */
|
|
92
|
+
export interface ReadingEntry {
|
|
93
|
+
/** `"<source>/<rule>"`. */
|
|
94
|
+
id: string;
|
|
95
|
+
source: string;
|
|
96
|
+
rule: string;
|
|
97
|
+
text: string;
|
|
98
|
+
/** Ids of the fact atoms this entry rests on -- the audit trail. */
|
|
99
|
+
atomIds: string[];
|
|
100
|
+
/** Sum of the matched atoms' salience times the rule weight. */
|
|
101
|
+
salience: number;
|
|
102
|
+
tags?: string[];
|
|
103
|
+
}
|
|
104
|
+
/** A resolved interpretation: ranked entries, each citing its facts. */
|
|
105
|
+
export interface Reading {
|
|
106
|
+
jdUt: number;
|
|
107
|
+
entries: ReadingEntry[];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run interpretation sources against a fact projection and return a ranked
|
|
111
|
+
* {@link Reading}. Each rule whose selector matches emits an entry carrying the
|
|
112
|
+
* matched atom ids (provenance) and a salience = sum of those atoms' salience x
|
|
113
|
+
* the rule weight. The engine never ships the content: the sources are the
|
|
114
|
+
* caller's.
|
|
115
|
+
*
|
|
116
|
+
* @param ctx A projection from {@link interpretationContext}.
|
|
117
|
+
* @param sources One or more {@link InterpretationSource} corpora.
|
|
118
|
+
* @returns The {@link Reading}; entries are sorted by descending salience.
|
|
119
|
+
*/
|
|
120
|
+
export declare function interpret(ctx: InterpretationContext, sources: InterpretationSource[]): Reading;
|
|
121
|
+
/** Entries about the same facts, gathered. */
|
|
122
|
+
export interface ReadingGroup {
|
|
123
|
+
/** Union of the group's cited atom ids -- the facts it is about. */
|
|
124
|
+
atomIds: string[];
|
|
125
|
+
/** Member entries, highest salience first. */
|
|
126
|
+
entries: ReadingEntry[];
|
|
127
|
+
/** Distinct tags across the members. */
|
|
128
|
+
tags: string[];
|
|
129
|
+
/** True when a declared conflicting tag-pair both appear (the corpus made
|
|
130
|
+
* opposing claims about the same facts). */
|
|
131
|
+
contested: boolean;
|
|
132
|
+
/** The group's salience (its strongest entry). */
|
|
133
|
+
salience: number;
|
|
134
|
+
}
|
|
135
|
+
export interface ReconcileOptions {
|
|
136
|
+
/** Tag pairs that contradict, e.g. `[["affirming", "challenging"]]`. */
|
|
137
|
+
conflicts?: [string, string][];
|
|
138
|
+
/** Drop an entry whose `text` duplicates a higher-salience one. */
|
|
139
|
+
dedupe?: boolean;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Group a {@link Reading}'s entries by the facts they share, so statements about
|
|
143
|
+
* the same atoms surface together rather than scattered through a flat list --
|
|
144
|
+
* the substrate for "everything said about this placement" and for spotting
|
|
145
|
+
* contention. Entries are connected when their cited atoms overlap; an entry
|
|
146
|
+
* citing nothing (an absence rule) stands alone. A group is `contested` when a
|
|
147
|
+
* declared conflicting tag-pair both appear in it.
|
|
148
|
+
*
|
|
149
|
+
* Semantic contradiction is the corpus author's to declare (via `tags` +
|
|
150
|
+
* `conflicts`); the resolver does the bookkeeping, not the judgement.
|
|
151
|
+
*
|
|
152
|
+
* @param reading A reading from {@link interpret}.
|
|
153
|
+
* @param opts Conflicting tag pairs and optional text de-duplication.
|
|
154
|
+
* @returns Groups sorted by descending salience.
|
|
155
|
+
*/
|
|
156
|
+
export declare function reconcile(reading: Reading, opts?: ReconcileOptions): ReadingGroup[];
|