caelus 0.17.0 → 0.19.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 +43 -0
- package/dist/src/chart.js +83 -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 +167 -0
- package/dist/src/interpret.js +192 -0
- package/dist/src/interpretation.d.ts +174 -0
- package/dist/src/interpretation.js +270 -0
- package/dist/src/node-loader.js +5 -2
- package/dist/src/provenance.d.ts +135 -0
- package/dist/src/provenance.js +159 -0
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine counterfactual -- a real chart, perturbed, and what changed.
|
|
3
|
+
*
|
|
4
|
+
* The `counterfactual` realm: take a resolved chart and ask "what if". Shift the
|
|
5
|
+
* instant ("born an hour later") or the place -- a real ephemeris recompute --
|
|
6
|
+
* or splice a body to a new longitude ("Mars in the next sign") -- a geometry
|
|
7
|
+
* what-if that keeps everything else and recomputes the aspects it touches.
|
|
8
|
+
* {@link chartDiff} reports the difference so the change is legible, not buried
|
|
9
|
+
* in two full charts.
|
|
10
|
+
*/
|
|
11
|
+
import { Engine, Chart, ChartOptions, Aspect } from "./chart.js";
|
|
12
|
+
import { AnchorRegistry } from "./provenance.js";
|
|
13
|
+
import { AnchoredChart, RealizedChart } from "./anchored.js";
|
|
14
|
+
/** A perturbation of a resolved chart. */
|
|
15
|
+
export interface CounterfactualEdit {
|
|
16
|
+
/** Shift the resolved instant by a duration (e.g. `"1h"`, `"-30m"`, `"P1D"`). */
|
|
17
|
+
shiftTime?: string;
|
|
18
|
+
/** Recompute at a different place. */
|
|
19
|
+
place?: {
|
|
20
|
+
lat: number;
|
|
21
|
+
lonEast: number;
|
|
22
|
+
altM?: number;
|
|
23
|
+
};
|
|
24
|
+
/** Move bodies to given ecliptic longitudes (degrees), keeping everything else
|
|
25
|
+
* -- a geometry what-if. The moved body's house and the touched aspects are
|
|
26
|
+
* recomputed; the angles and other bodies are untouched. */
|
|
27
|
+
setLongitudes?: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
/** A body whose sign or house changed between two charts. */
|
|
30
|
+
export interface BodyChange {
|
|
31
|
+
body: string;
|
|
32
|
+
/** Signed degrees the body moved (`b` minus `a`). */
|
|
33
|
+
dLon: number;
|
|
34
|
+
signFrom: string;
|
|
35
|
+
signTo: string;
|
|
36
|
+
houseFrom: number;
|
|
37
|
+
houseTo: number;
|
|
38
|
+
}
|
|
39
|
+
/** An angle whose sign changed. */
|
|
40
|
+
export interface AngleChange {
|
|
41
|
+
angle: string;
|
|
42
|
+
from: string;
|
|
43
|
+
to: string;
|
|
44
|
+
}
|
|
45
|
+
/** What differs between two charts. */
|
|
46
|
+
export interface ChartDiff {
|
|
47
|
+
/** Bodies whose sign or house changed. */
|
|
48
|
+
bodies: BodyChange[];
|
|
49
|
+
/** Aspects present in the variant but not the original. */
|
|
50
|
+
aspectsGained: Aspect[];
|
|
51
|
+
/** Aspects present in the original but not the variant. */
|
|
52
|
+
aspectsLost: Aspect[];
|
|
53
|
+
/** Angles whose sign changed. */
|
|
54
|
+
angles: AngleChange[];
|
|
55
|
+
}
|
|
56
|
+
/** Diff two charts: body sign/house shifts, aspects gained/lost, angle sign
|
|
57
|
+
* changes. Bodies and angles that did not change sign/house are omitted. */
|
|
58
|
+
export declare function chartDiff(a: Chart, b: Chart): ChartDiff;
|
|
59
|
+
/** A counterfactual: a base chart and a perturbed variant, with the diff. */
|
|
60
|
+
export interface Counterfactual {
|
|
61
|
+
edit: CounterfactualEdit;
|
|
62
|
+
/** The realized base (its `chart` is the original). */
|
|
63
|
+
original: RealizedChart;
|
|
64
|
+
/** The perturbed chart, or null when the base had no chart to perturb. */
|
|
65
|
+
variant: Chart | null;
|
|
66
|
+
diff: ChartDiff | null;
|
|
67
|
+
note: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Realize an {@link AnchoredChart}, then apply a {@link CounterfactualEdit} and
|
|
71
|
+
* diff the result -- "a real event, perturbed." A time/place edit recomputes the
|
|
72
|
+
* ephemeris; a `setLongitudes` edit splices the geometry.
|
|
73
|
+
*
|
|
74
|
+
* @param engine The engine.
|
|
75
|
+
* @param base The base chart to perturb (realized via {@link realize}).
|
|
76
|
+
* @param edit The perturbation.
|
|
77
|
+
* @param registry Anchor lookups for the base.
|
|
78
|
+
* @param opts Chart options (house system, zodiac).
|
|
79
|
+
* @returns A {@link Counterfactual}; `variant`/`diff` are null when the base
|
|
80
|
+
* produced no chart (e.g. a constraints-only form).
|
|
81
|
+
*/
|
|
82
|
+
export declare function counterfactual(engine: Engine, base: AnchoredChart, edit: CounterfactualEdit, registry?: AnchorRegistry, opts?: ChartOptions): Counterfactual;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine counterfactual -- a real chart, perturbed, and what changed.
|
|
3
|
+
*
|
|
4
|
+
* The `counterfactual` realm: take a resolved chart and ask "what if". Shift the
|
|
5
|
+
* instant ("born an hour later") or the place -- a real ephemeris recompute --
|
|
6
|
+
* or splice a body to a new longitude ("Mars in the next sign") -- a geometry
|
|
7
|
+
* what-if that keeps everything else and recomputes the aspects it touches.
|
|
8
|
+
* {@link chartDiff} reports the difference so the change is legible, not buried
|
|
9
|
+
* in two full charts.
|
|
10
|
+
*/
|
|
11
|
+
import { SIGNS, findAspects, DEFAULT_ORBS, } from "./chart.js";
|
|
12
|
+
import { houseOf } from "./electional.js";
|
|
13
|
+
import { mod } from "./core.js";
|
|
14
|
+
import { parseOffset } from "./provenance.js";
|
|
15
|
+
import { realize } from "./anchored.js";
|
|
16
|
+
const signOf = (lon) => SIGNS[Math.floor(mod(lon, 360) / 30)];
|
|
17
|
+
const aspectKey = (x) => `${[x.a, x.b].sort().join("~")}:${x.aspect}`;
|
|
18
|
+
/** Diff two charts: body sign/house shifts, aspects gained/lost, angle sign
|
|
19
|
+
* changes. Bodies and angles that did not change sign/house are omitted. */
|
|
20
|
+
export function chartDiff(a, b) {
|
|
21
|
+
const bodies = [];
|
|
22
|
+
for (const [name, pa] of Object.entries(a.bodies)) {
|
|
23
|
+
const pb = b.bodies[name];
|
|
24
|
+
if (!pa || !pb || (pa.sign === pb.sign && pa.house === pb.house))
|
|
25
|
+
continue;
|
|
26
|
+
bodies.push({
|
|
27
|
+
body: name, dLon: mod(pb.lon - pa.lon + 180, 360) - 180,
|
|
28
|
+
signFrom: pa.sign, signTo: pb.sign, houseFrom: pa.house, houseTo: pb.house,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const aset = new Set(a.aspects.map(aspectKey));
|
|
32
|
+
const bset = new Set(b.aspects.map(aspectKey));
|
|
33
|
+
const angles = [];
|
|
34
|
+
for (const ang of ["asc", "mc", "vertex", "eastPoint"]) {
|
|
35
|
+
const from = signOf(a.angles[ang]);
|
|
36
|
+
const to = signOf(b.angles[ang]);
|
|
37
|
+
if (from !== to)
|
|
38
|
+
angles.push({ angle: ang, from, to });
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
bodies,
|
|
42
|
+
aspectsGained: b.aspects.filter((x) => !aset.has(aspectKey(x))),
|
|
43
|
+
aspectsLost: a.aspects.filter((x) => !bset.has(aspectKey(x))),
|
|
44
|
+
angles,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Splice bodies to new longitudes, recomputing their sign/house and the
|
|
48
|
+
* aspects (the angles and untouched bodies stay as they were). */
|
|
49
|
+
function spliceLongitudes(chart, overrides) {
|
|
50
|
+
const bodies = { ...chart.bodies };
|
|
51
|
+
for (const [name, lon] of Object.entries(overrides)) {
|
|
52
|
+
const cur = bodies[name];
|
|
53
|
+
if (!cur)
|
|
54
|
+
continue;
|
|
55
|
+
const L = mod(lon, 360);
|
|
56
|
+
bodies[name] = {
|
|
57
|
+
...cur, lon: L, sign: SIGNS[Math.floor(L / 30)], signDeg: L % 30,
|
|
58
|
+
house: houseOf(L, chart.cusps),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...chart, bodies: bodies,
|
|
63
|
+
aspects: findAspects(bodies, DEFAULT_ORBS),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Realize an {@link AnchoredChart}, then apply a {@link CounterfactualEdit} and
|
|
68
|
+
* diff the result -- "a real event, perturbed." A time/place edit recomputes the
|
|
69
|
+
* ephemeris; a `setLongitudes` edit splices the geometry.
|
|
70
|
+
*
|
|
71
|
+
* @param engine The engine.
|
|
72
|
+
* @param base The base chart to perturb (realized via {@link realize}).
|
|
73
|
+
* @param edit The perturbation.
|
|
74
|
+
* @param registry Anchor lookups for the base.
|
|
75
|
+
* @param opts Chart options (house system, zodiac).
|
|
76
|
+
* @returns A {@link Counterfactual}; `variant`/`diff` are null when the base
|
|
77
|
+
* produced no chart (e.g. a constraints-only form).
|
|
78
|
+
*/
|
|
79
|
+
export function counterfactual(engine, base, edit, registry = {}, opts = {}) {
|
|
80
|
+
const original = realize(engine, base, registry, opts);
|
|
81
|
+
if (!original.chart) {
|
|
82
|
+
return { edit, original, variant: null, diff: null, note: `nothing to perturb (${original.note})` };
|
|
83
|
+
}
|
|
84
|
+
const shiftsTimeOrPlace = edit.shiftTime !== undefined || edit.place !== undefined;
|
|
85
|
+
let variant = original.chart;
|
|
86
|
+
if (shiftsTimeOrPlace) {
|
|
87
|
+
const off = edit.shiftTime !== undefined ? parseOffset(edit.shiftTime) : 0;
|
|
88
|
+
if (Number.isNaN(off))
|
|
89
|
+
throw new Error(`unparseable shiftTime ${edit.shiftTime}`);
|
|
90
|
+
const lat = edit.place?.lat ?? original.place.place?.lat ?? 0;
|
|
91
|
+
const lon = edit.place?.lonEast ?? original.place.place?.lonEast ?? 0;
|
|
92
|
+
variant = engine.chartAt(original.time.jd + off, lat, lon, opts);
|
|
93
|
+
}
|
|
94
|
+
if (edit.setLongitudes)
|
|
95
|
+
variant = spliceLongitudes(variant, edit.setLongitudes);
|
|
96
|
+
const bits = [
|
|
97
|
+
edit.shiftTime !== undefined ? `time ${edit.shiftTime}` : null,
|
|
98
|
+
edit.place ? "place" : null,
|
|
99
|
+
edit.setLongitudes ? `moved ${Object.keys(edit.setLongitudes).join(", ")}` : null,
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
return {
|
|
102
|
+
edit, original, variant, diff: chartDiff(original.chart, variant),
|
|
103
|
+
note: bits.length ? `perturbed: ${bits.join("; ")}` : "no edit applied",
|
|
104
|
+
};
|
|
105
|
+
}
|
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";
|