caelus 0.2.1 → 0.4.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 +14 -6
- package/accuracy.json +234 -25
- package/dist/src/chart.d.ts +59 -11
- package/dist/src/chart.js +238 -46
- package/dist/src/core.d.ts +57 -1
- package/dist/src/core.js +168 -21
- package/dist/src/events.d.ts +22 -0
- package/dist/src/events.js +156 -0
- package/dist/src/houses.d.ts +31 -0
- package/dist/src/houses.js +205 -2
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/node-loader.js +21 -2
- package/dist/src/pheno.d.ts +35 -0
- package/dist/src/pheno.js +152 -0
- package/package.json +1 -1
|
@@ -0,0 +1,156 @@
|
|
|
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 (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
|
+
}
|
package/dist/src/houses.d.ts
CHANGED
|
@@ -4,11 +4,42 @@ import { EngineData } from "./core.js";
|
|
|
4
4
|
export declare function gmst(jdUt: number): number;
|
|
5
5
|
/** Greenwich apparent sidereal time. */
|
|
6
6
|
export declare function gast(data: EngineData, jdUt: number): number;
|
|
7
|
+
/** Ecliptic longitude where the house circle with pole `pole` crosses the
|
|
8
|
+
* ecliptic; `ra` measured like ARMC. The Ascendant is houseCusp(armc+90,
|
|
9
|
+
* phi); the MC is houseCusp(armc, 0). */
|
|
10
|
+
export declare function houseCusp(ra: number, pole: number, eps: number): number;
|
|
7
11
|
/** Ascendant, MC, ARMC, obliquity. East longitude positive. */
|
|
8
12
|
export declare function angles(data: EngineData, jdUt: number, latDeg: number, lonDeg: number): [number, number, number, number];
|
|
13
|
+
/** Vertex (western crossing of prime vertical and ecliptic) and east
|
|
14
|
+
* point (equatorial ascendant). Radians in, radians out. */
|
|
15
|
+
export declare function vertexEastPoint(armc: number, phi: number, eps: number): [number, number];
|
|
9
16
|
export declare function housesWholeSign(asc: number): number[];
|
|
10
17
|
export declare function housesEqual(asc: number): number[];
|
|
11
18
|
export declare function housesPorphyry(asc: number, mc: number): number[];
|
|
19
|
+
/** Koch (birthplace): cusps are ascendants at ARMC +/- k/3 of the MC
|
|
20
|
+
* degree's diurnal semi-arc. Throws where the MC degree is circumpolar
|
|
21
|
+
* (|phi| >= 90 - eps, matching Swiss Ephemeris). */
|
|
22
|
+
export declare function housesKoch(armc: number, phi: number, eps: number): number[];
|
|
23
|
+
/** Regiomontanus: equal divisions of the celestial equator; cusp poles
|
|
24
|
+
* tan P = tan(phi) sin(H). */
|
|
25
|
+
export declare function housesRegiomontanus(armc: number, phi: number, eps: number): number[];
|
|
26
|
+
/** Campanus: equal divisions of the prime vertical. House circles run
|
|
27
|
+
* through the horizon's north/south points; cusps are their ecliptic
|
|
28
|
+
* crossings, assigned in zodiacal order MC->ASC->IC. */
|
|
29
|
+
export declare function housesCampanus(armc: number, phi: number, eps: number): number[];
|
|
30
|
+
/** Alcabitius: trisect the Ascendant degree's semi-arcs in right ascension;
|
|
31
|
+
* project cusps along meridians. */
|
|
32
|
+
export declare function housesAlcabitius(armc: number, phi: number, eps: number): number[];
|
|
33
|
+
/** Morinus: equal RA divisions projected onto the ecliptic by great circles
|
|
34
|
+
* through the ecliptic poles. Latitude-independent. */
|
|
35
|
+
export declare function housesMorinus(armc: number, _phi: number, eps: number): number[];
|
|
36
|
+
/** Meridian (axial rotation): equal RA divisions projected along hour
|
|
37
|
+
* circles. Latitude-independent. */
|
|
38
|
+
export declare function housesMeridian(armc: number, _phi: number, eps: number): number[];
|
|
39
|
+
/** Polich-Page ('topocentric'): cusp poles tan P = (k/3) tan(phi). */
|
|
40
|
+
export declare function housesPolichPage(armc: number, phi: number, eps: number): number[];
|
|
41
|
+
/** Vehlow: equal houses with the ASC at the middle of house 1. */
|
|
42
|
+
export declare function housesVehlow(armc: number, phi: number, eps: number): number[];
|
|
12
43
|
/**
|
|
13
44
|
* Placidus cusps via the classic iterative scheme. Semi-arc derivation:
|
|
14
45
|
* for ALL four intermediate cusps RA = ARMC + offset + f*AD with
|
package/dist/src/houses.js
CHANGED
|
@@ -15,16 +15,49 @@ export function gast(data, jdUt) {
|
|
|
15
15
|
const eps = trueObliquity(data, jde);
|
|
16
16
|
return mod(gmst(jdUt) + dpsi * Math.cos(eps), TWO_PI);
|
|
17
17
|
}
|
|
18
|
+
/** Ecliptic longitude where the house circle with pole `pole` crosses the
|
|
19
|
+
* ecliptic; `ra` measured like ARMC. The Ascendant is houseCusp(armc+90,
|
|
20
|
+
* phi); the MC is houseCusp(armc, 0). */
|
|
21
|
+
export function houseCusp(ra, pole, eps) {
|
|
22
|
+
return mod(Math.atan2(Math.sin(ra), Math.cos(ra) * Math.cos(eps) - Math.sin(eps) * Math.tan(pole)), TWO_PI);
|
|
23
|
+
}
|
|
24
|
+
function mcOf(armc, eps) {
|
|
25
|
+
return mod(Math.atan2(Math.sin(armc), Math.cos(armc) * Math.cos(eps)), TWO_PI);
|
|
26
|
+
}
|
|
27
|
+
/** Ascendant with the polar-latitude convention: the ASC always lies in
|
|
28
|
+
* the half-circle (MC, MC+180). Above ~66 deg the raw horizon intersection
|
|
29
|
+
* can be the setting one; Swiss Ephemeris applies the same correction. */
|
|
30
|
+
function ascOf(armc, phi, eps) {
|
|
31
|
+
let asc = houseCusp(armc + Math.PI / 2, phi, eps);
|
|
32
|
+
if (mod(asc - mcOf(armc, eps), TWO_PI) >= Math.PI) {
|
|
33
|
+
asc = mod(asc + Math.PI, TWO_PI);
|
|
34
|
+
}
|
|
35
|
+
return asc;
|
|
36
|
+
}
|
|
18
37
|
/** Ascendant, MC, ARMC, obliquity. East longitude positive. */
|
|
19
38
|
export function angles(data, jdUt, latDeg, lonDeg) {
|
|
20
39
|
const jde = jdTT(jdUt);
|
|
21
40
|
const eps = trueObliquity(data, jde);
|
|
22
41
|
const armc = mod(gast(data, jdUt) + lonDeg * DEG, TWO_PI);
|
|
23
42
|
const phi = latDeg * DEG;
|
|
24
|
-
const mc =
|
|
25
|
-
const asc =
|
|
43
|
+
const mc = mcOf(armc, eps);
|
|
44
|
+
const asc = ascOf(armc, phi, eps);
|
|
26
45
|
return [asc, mc, armc, eps];
|
|
27
46
|
}
|
|
47
|
+
/** Vertex (western crossing of prime vertical and ecliptic) and east
|
|
48
|
+
* point (equatorial ascendant). Radians in, radians out. */
|
|
49
|
+
export function vertexEastPoint(armc, phi, eps) {
|
|
50
|
+
const colat = phi >= 0 ? Math.PI / 2 - phi : -Math.PI / 2 - phi;
|
|
51
|
+
let vtx = houseCusp(armc + (3 * Math.PI) / 2, colat, eps);
|
|
52
|
+
// pick the western intersection: equatorial direction . east-point < 0
|
|
53
|
+
const dx = Math.cos(vtx);
|
|
54
|
+
const dy = Math.sin(vtx) * Math.cos(eps);
|
|
55
|
+
if (dx * -Math.sin(armc) + dy * Math.cos(armc) > 0) {
|
|
56
|
+
vtx = mod(vtx + Math.PI, TWO_PI);
|
|
57
|
+
}
|
|
58
|
+
const east = houseCusp(armc + Math.PI / 2, 0.0, eps);
|
|
59
|
+
return [vtx, east];
|
|
60
|
+
}
|
|
28
61
|
export function housesWholeSign(asc) {
|
|
29
62
|
const first = Math.floor(asc / (30 * DEG)) * 30 * DEG;
|
|
30
63
|
return Array.from({ length: 12 }, (_, i) => mod(first + i * 30 * DEG, TWO_PI));
|
|
@@ -53,6 +86,176 @@ export function housesPorphyry(asc, mc) {
|
|
|
53
86
|
cusps[8] = mod(cusps[2] + Math.PI, TWO_PI);
|
|
54
87
|
return cusps;
|
|
55
88
|
}
|
|
89
|
+
function signed(x) {
|
|
90
|
+
return mod(x + Math.PI, TWO_PI) - Math.PI;
|
|
91
|
+
}
|
|
92
|
+
function fillOpposites(out) {
|
|
93
|
+
for (const k of [3, 4, 5])
|
|
94
|
+
out[k] = mod(out[k + 6] + Math.PI, TWO_PI);
|
|
95
|
+
for (const k of [6, 7, 8])
|
|
96
|
+
out[k] = mod(out[k - 6] + Math.PI, TWO_PI);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/** Cusps 1 and 10. With flipMc (Regiomontanus, Campanus, Polich-Page), the
|
|
100
|
+
* MC moves to the IC when the polar ASC correction fires, keeping the cusps
|
|
101
|
+
* in zodiacal order; Swiss Ephemeris does the same. Alcabitius and Koch
|
|
102
|
+
* keep the astronomical MC. */
|
|
103
|
+
function quadrantFrame(armc, phi, eps, flipMc) {
|
|
104
|
+
const out = new Array(12).fill(0);
|
|
105
|
+
let mc = mcOf(armc, eps);
|
|
106
|
+
let asc = houseCusp(armc + Math.PI / 2, phi, eps);
|
|
107
|
+
if (mod(asc - mc, TWO_PI) >= Math.PI) {
|
|
108
|
+
asc = mod(asc + Math.PI, TWO_PI);
|
|
109
|
+
if (flipMc)
|
|
110
|
+
mc = mod(mc + Math.PI, TWO_PI);
|
|
111
|
+
}
|
|
112
|
+
out[0] = asc;
|
|
113
|
+
out[9] = mc;
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/** Every quadrant-system house circle passes through the horizon's
|
|
117
|
+
* north/south points, so its two ecliptic crossings sit east and west of
|
|
118
|
+
* the meridian. Cusps 11, 12, 2, 3 are the eastern ones. */
|
|
119
|
+
function eastOfMeridian(lon, armc, eps) {
|
|
120
|
+
const ra = Math.atan2(Math.sin(lon) * Math.cos(eps), Math.cos(lon));
|
|
121
|
+
if (Math.sin(armc - ra) > 0)
|
|
122
|
+
return mod(lon + Math.PI, TWO_PI);
|
|
123
|
+
return lon;
|
|
124
|
+
}
|
|
125
|
+
/** Force a cusp candidate onto the short arc from lo spanning the signed
|
|
126
|
+
* angle d (negative when the polar ASC correction reverses the zodiacal
|
|
127
|
+
* direction of the house sequence). */
|
|
128
|
+
function normArc(lon, lo, d) {
|
|
129
|
+
const off = signed(lon - lo);
|
|
130
|
+
const inside = d >= 0 ? off >= 0 && off <= d : off >= d && off <= 0;
|
|
131
|
+
return inside ? lon : mod(lon + Math.PI, TWO_PI);
|
|
132
|
+
}
|
|
133
|
+
/** Koch (birthplace): cusps are ascendants at ARMC +/- k/3 of the MC
|
|
134
|
+
* degree's diurnal semi-arc. Throws where the MC degree is circumpolar
|
|
135
|
+
* (|phi| >= 90 - eps, matching Swiss Ephemeris). */
|
|
136
|
+
export function housesKoch(armc, phi, eps) {
|
|
137
|
+
if (Math.abs(phi) >= Math.PI / 2 - eps) {
|
|
138
|
+
throw new RangeError("koch undefined at polar latitudes");
|
|
139
|
+
}
|
|
140
|
+
const out = quadrantFrame(armc, phi, eps, false);
|
|
141
|
+
const decMc = Math.asin(Math.sin(eps) * Math.sin(out[9]));
|
|
142
|
+
const x = Math.tan(phi) * Math.tan(decMc);
|
|
143
|
+
if (Math.abs(x) > 1) {
|
|
144
|
+
throw new RangeError("koch undefined: MC degree circumpolar");
|
|
145
|
+
}
|
|
146
|
+
const sa = Math.PI / 2 + Math.asin(x); // diurnal semi-arc of the MC degree
|
|
147
|
+
out[10] = ascOf(armc - (2 * sa) / 3, phi, eps);
|
|
148
|
+
out[11] = ascOf(armc - sa / 3, phi, eps);
|
|
149
|
+
out[1] = ascOf(armc + sa / 3, phi, eps);
|
|
150
|
+
out[2] = ascOf(armc + (2 * sa) / 3, phi, eps);
|
|
151
|
+
return fillOpposites(out);
|
|
152
|
+
}
|
|
153
|
+
/** Regiomontanus: equal divisions of the celestial equator; cusp poles
|
|
154
|
+
* tan P = tan(phi) sin(H). */
|
|
155
|
+
export function housesRegiomontanus(armc, phi, eps) {
|
|
156
|
+
const out = quadrantFrame(armc, phi, eps, true);
|
|
157
|
+
for (const [k, h] of [[10, 30], [11, 60], [1, 120], [2, 150]]) {
|
|
158
|
+
const pole = Math.atan(Math.tan(phi) * Math.sin(h * DEG));
|
|
159
|
+
out[k] = eastOfMeridian(houseCusp(armc + h * DEG, pole, eps), armc, eps);
|
|
160
|
+
}
|
|
161
|
+
return fillOpposites(out);
|
|
162
|
+
}
|
|
163
|
+
/** Campanus: equal divisions of the prime vertical. House circles run
|
|
164
|
+
* through the horizon's north/south points; cusps are their ecliptic
|
|
165
|
+
* crossings, assigned in zodiacal order MC->ASC->IC. */
|
|
166
|
+
export function housesCampanus(armc, phi, eps) {
|
|
167
|
+
const out = quadrantFrame(armc, phi, eps, true);
|
|
168
|
+
const n = [
|
|
169
|
+
-Math.sin(phi) * Math.cos(armc), -Math.sin(phi) * Math.sin(armc), Math.cos(phi),
|
|
170
|
+
];
|
|
171
|
+
const zen = [
|
|
172
|
+
Math.cos(phi) * Math.cos(armc), Math.cos(phi) * Math.sin(armc), Math.sin(phi),
|
|
173
|
+
];
|
|
174
|
+
const east = [-Math.sin(armc), Math.cos(armc), 0.0];
|
|
175
|
+
const pole = [0.0, -Math.sin(eps), Math.cos(eps)];
|
|
176
|
+
const cusp = (theta) => {
|
|
177
|
+
const t = theta * DEG;
|
|
178
|
+
const v = [
|
|
179
|
+
east[0] * Math.cos(t) + zen[0] * Math.sin(t),
|
|
180
|
+
east[1] * Math.cos(t) + zen[1] * Math.sin(t),
|
|
181
|
+
east[2] * Math.cos(t) + zen[2] * Math.sin(t),
|
|
182
|
+
];
|
|
183
|
+
const m = [
|
|
184
|
+
n[1] * v[2] - n[2] * v[1], n[2] * v[0] - n[0] * v[2], n[0] * v[1] - n[1] * v[0],
|
|
185
|
+
];
|
|
186
|
+
const d = [
|
|
187
|
+
m[1] * pole[2] - m[2] * pole[1],
|
|
188
|
+
m[2] * pole[0] - m[0] * pole[2],
|
|
189
|
+
m[0] * pole[1] - m[1] * pole[0],
|
|
190
|
+
];
|
|
191
|
+
return mod(Math.atan2(d[1] * Math.cos(eps) + d[2] * Math.sin(eps), d[0]), TWO_PI);
|
|
192
|
+
};
|
|
193
|
+
for (const [k, theta] of [[10, 30], [11, 60], [1, 120], [2, 150]]) {
|
|
194
|
+
out[k] = cusp(theta);
|
|
195
|
+
}
|
|
196
|
+
const mc = out[9];
|
|
197
|
+
const asc = out[0];
|
|
198
|
+
const dUp = signed(asc - mc);
|
|
199
|
+
const dDn = signed(mod(mc + Math.PI, TWO_PI) - asc);
|
|
200
|
+
for (const k of [10, 11])
|
|
201
|
+
out[k] = normArc(out[k], mc, dUp);
|
|
202
|
+
for (const k of [1, 2])
|
|
203
|
+
out[k] = normArc(out[k], asc, dDn);
|
|
204
|
+
// within each quadrant the two cusps must be in house order (away from
|
|
205
|
+
// MC, away from ASC)
|
|
206
|
+
if (Math.abs(signed(out[10] - mc)) > Math.abs(signed(out[11] - mc))) {
|
|
207
|
+
[out[10], out[11]] = [out[11], out[10]];
|
|
208
|
+
}
|
|
209
|
+
if (Math.abs(signed(out[1] - asc)) > Math.abs(signed(out[2] - asc))) {
|
|
210
|
+
[out[1], out[2]] = [out[2], out[1]];
|
|
211
|
+
}
|
|
212
|
+
return fillOpposites(out);
|
|
213
|
+
}
|
|
214
|
+
/** Alcabitius: trisect the Ascendant degree's semi-arcs in right ascension;
|
|
215
|
+
* project cusps along meridians. */
|
|
216
|
+
export function housesAlcabitius(armc, phi, eps) {
|
|
217
|
+
const out = quadrantFrame(armc, phi, eps, false);
|
|
218
|
+
const dec = Math.asin(Math.sin(eps) * Math.sin(out[0]));
|
|
219
|
+
const x = Math.max(-1.0, Math.min(1.0, Math.tan(phi) * Math.tan(dec)));
|
|
220
|
+
const ad = Math.asin(x);
|
|
221
|
+
const sda = Math.PI / 2 + ad; // diurnal semi-arc of the ASC degree
|
|
222
|
+
const sna = Math.PI / 2 - ad;
|
|
223
|
+
const ras = [
|
|
224
|
+
[10, armc + sda / 3], [11, armc + (2 * sda) / 3],
|
|
225
|
+
[1, armc + Math.PI - (2 * sna) / 3], [2, armc + Math.PI - sna / 3],
|
|
226
|
+
];
|
|
227
|
+
for (const [k, ra] of ras) {
|
|
228
|
+
out[k] = mod(Math.atan2(Math.sin(ra), Math.cos(ra) * Math.cos(eps)), TWO_PI);
|
|
229
|
+
}
|
|
230
|
+
return fillOpposites(out);
|
|
231
|
+
}
|
|
232
|
+
/** Morinus: equal RA divisions projected onto the ecliptic by great circles
|
|
233
|
+
* through the ecliptic poles. Latitude-independent. */
|
|
234
|
+
export function housesMorinus(armc, _phi, eps) {
|
|
235
|
+
return Array.from({ length: 12 }, (_, i) => mod(Math.atan2(Math.sin(armc + (i + 3) * 30 * DEG) * Math.cos(eps), Math.cos(armc + (i + 3) * 30 * DEG)), TWO_PI));
|
|
236
|
+
}
|
|
237
|
+
/** Meridian (axial rotation): equal RA divisions projected along hour
|
|
238
|
+
* circles. Latitude-independent. */
|
|
239
|
+
export function housesMeridian(armc, _phi, eps) {
|
|
240
|
+
return Array.from({ length: 12 }, (_, i) => mod(Math.atan2(Math.sin(armc + (i + 3) * 30 * DEG), Math.cos(armc + (i + 3) * 30 * DEG) * Math.cos(eps)), TWO_PI));
|
|
241
|
+
}
|
|
242
|
+
/** Polich-Page ('topocentric'): cusp poles tan P = (k/3) tan(phi). */
|
|
243
|
+
export function housesPolichPage(armc, phi, eps) {
|
|
244
|
+
const out = quadrantFrame(armc, phi, eps, true);
|
|
245
|
+
const spec = [
|
|
246
|
+
[10, 30, 1], [11, 60, 2], [1, 120, 2], [2, 150, 1],
|
|
247
|
+
];
|
|
248
|
+
for (const [k, h, w] of spec) {
|
|
249
|
+
const pole = Math.atan((Math.tan(phi) * w) / 3.0);
|
|
250
|
+
out[k] = eastOfMeridian(houseCusp(armc + h * DEG, pole, eps), armc, eps);
|
|
251
|
+
}
|
|
252
|
+
return fillOpposites(out);
|
|
253
|
+
}
|
|
254
|
+
/** Vehlow: equal houses with the ASC at the middle of house 1. */
|
|
255
|
+
export function housesVehlow(armc, phi, eps) {
|
|
256
|
+
const asc = ascOf(armc, phi, eps);
|
|
257
|
+
return Array.from({ length: 12 }, (_, i) => mod(asc - 15 * DEG + i * 30 * DEG, TWO_PI));
|
|
258
|
+
}
|
|
56
259
|
/**
|
|
57
260
|
* Placidus cusps via the classic iterative scheme. Semi-arc derivation:
|
|
58
261
|
* for ALL four intermediate cusps RA = ARMC + offset + f*AD with
|
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,15 @@ 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
|
+
// asteroid packs (Horizons fits): loaded when present, ~380 KB total
|
|
35
|
+
for (const b of ["ceres", "pallas", "juno", "vesta", "pholus"]) {
|
|
36
|
+
if (existsSync(join(dir, `${b}_cheb.json`))) {
|
|
37
|
+
(data.chebPacks ??= {})[b] = j(`${b}_cheb.json`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
21
40
|
if (moonTier !== "none") {
|
|
22
41
|
// The npm package ships only the embedded tier (1920-2080); the full
|
|
23
42
|
// tier (1850-2150, 3.1 MB, same precision) lives in the repo. Fall back
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine pheno -- phase, elongation, apparent diameter, magnitude,
|
|
3
|
+
* equation of time, horizontal coordinates, refraction.
|
|
4
|
+
*
|
|
5
|
+
* Magnitude models: Mallama & Hilton 2018 for Mercury-Saturn (Saturn with
|
|
6
|
+
* the ring term), constant-plus-distance for Sun and Pluto, the Mallama
|
|
7
|
+
* secular ramp for Neptune, Allen's phase law for the Moon (valid to phase
|
|
8
|
+
* angle ~140 deg; the Moon is invisible near conjunction anyway).
|
|
9
|
+
* Validated against swe_pheno (Swiss Ephemeris 2.10, Moshier mode).
|
|
10
|
+
*/
|
|
11
|
+
import { EngineData } from "./core.js";
|
|
12
|
+
import { Engine, BodyId } from "./chart.js";
|
|
13
|
+
/** Equatorial diameters, km (IAU values, as used by Swiss Ephemeris). */
|
|
14
|
+
export declare const DIAMETER_KM: Record<string, number>;
|
|
15
|
+
export interface Pheno {
|
|
16
|
+
phaseAngle: number;
|
|
17
|
+
phase: number;
|
|
18
|
+
elongation: number;
|
|
19
|
+
diameter: number;
|
|
20
|
+
magnitude: number;
|
|
21
|
+
}
|
|
22
|
+
/** Phase angle (deg), illuminated fraction, elongation (deg), apparent
|
|
23
|
+
* diameter (deg), apparent magnitude. */
|
|
24
|
+
export declare function pheno(engine: Engine, body: BodyId, jdUt: number): Pheno;
|
|
25
|
+
/** Apparent minus mean solar time, minutes (Meeus ch. 28). */
|
|
26
|
+
export declare function equationOfTime(engine: Engine, jdUt: number): number;
|
|
27
|
+
/** Apparent ecliptic position -> azimuth (deg, from true north, east-
|
|
28
|
+
* positive) and true altitude (deg). No refraction. */
|
|
29
|
+
export declare function azAlt(data: EngineData, lonDeg: number, latDeg: number, jdUt: number, obsLat: number, obsLonEast: number): [number, number];
|
|
30
|
+
/** Saemundsson refraction, degrees. Returns the input unchanged when even
|
|
31
|
+
* the refracted altitude stays below the horizon (matches Swiss
|
|
32
|
+
* Ephemeris). */
|
|
33
|
+
export declare function refractTrueToApparent(altDeg: number, pressure?: number, tempC?: number): number;
|
|
34
|
+
/** Bennett refraction, degrees. */
|
|
35
|
+
export declare function refractApparentToTrue(altDeg: number, pressure?: number, tempC?: number): number;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine pheno -- phase, elongation, apparent diameter, magnitude,
|
|
3
|
+
* equation of time, horizontal coordinates, refraction.
|
|
4
|
+
*
|
|
5
|
+
* Magnitude models: Mallama & Hilton 2018 for Mercury-Saturn (Saturn with
|
|
6
|
+
* the ring term), constant-plus-distance for Sun and Pluto, the Mallama
|
|
7
|
+
* secular ramp for Neptune, Allen's phase law for the Moon (valid to phase
|
|
8
|
+
* angle ~140 deg; the Moon is invisible near conjunction anyway).
|
|
9
|
+
* Validated against swe_pheno (Swiss Ephemeris 2.10, Moshier mode).
|
|
10
|
+
*/
|
|
11
|
+
import { DEG, J2000, mod, jdTT, trueObliquity, equatorial, sunApparent, } from "./core.js";
|
|
12
|
+
import { gast } from "./houses.js";
|
|
13
|
+
const TWO_PI = 2 * Math.PI;
|
|
14
|
+
const KM_PER_AU = 149597870.7;
|
|
15
|
+
/** Equatorial diameters, km (IAU values, as used by Swiss Ephemeris). */
|
|
16
|
+
export const DIAMETER_KM = {
|
|
17
|
+
sun: 1392000.0, moon: 3475.0, mercury: 4878.8, venus: 12103.6,
|
|
18
|
+
mars: 6779.0, jupiter: 139822.0, saturn: 116464.0,
|
|
19
|
+
uranus: 50724.0, neptune: 49244.0, pluto: 2376.6,
|
|
20
|
+
};
|
|
21
|
+
function magnitude(body, a, r, dlt, jde, lonDeg, latDeg) {
|
|
22
|
+
const x = 5 * Math.log10(r * dlt);
|
|
23
|
+
switch (body) {
|
|
24
|
+
case "sun":
|
|
25
|
+
return -26.86 + 5 * Math.log10(dlt);
|
|
26
|
+
case "moon":
|
|
27
|
+
// Allen phase law; constant solved against swe_pheno (a < 130).
|
|
28
|
+
return 0.233431 + x + 0.026 * Math.abs(a) + 4e-9 * a ** 4;
|
|
29
|
+
case "mercury":
|
|
30
|
+
return x - 0.613 + 6.328e-2 * a - 1.6336e-3 * a ** 2 + 3.3644e-5 * a ** 3
|
|
31
|
+
- 3.4265e-7 * a ** 4 + 1.6893e-9 * a ** 5 - 3.0334e-12 * a ** 6;
|
|
32
|
+
case "venus":
|
|
33
|
+
if (a <= 163.7) {
|
|
34
|
+
return x - 4.384 - 1.044e-3 * a + 3.687e-4 * a ** 2
|
|
35
|
+
- 2.814e-6 * a ** 3 + 8.938e-9 * a ** 4;
|
|
36
|
+
}
|
|
37
|
+
return x + 236.05828 - 2.81914 * a + 8.39034e-3 * a ** 2;
|
|
38
|
+
case "mars":
|
|
39
|
+
return x - 1.601 + 2.267e-2 * a - 1.302e-4 * a ** 2;
|
|
40
|
+
case "jupiter":
|
|
41
|
+
return x - 9.395 - 3.7e-4 * a + 6.16e-4 * a ** 2;
|
|
42
|
+
case "saturn": {
|
|
43
|
+
// ring inclination (Meeus ch. 45)
|
|
44
|
+
const T = (jde - J2000) / 36525.0;
|
|
45
|
+
const i = (28.075216 - 0.012998 * T + 0.000004 * T * T) * DEG;
|
|
46
|
+
const om = (169.50847 + 1.394681 * T + 0.000412 * T * T) * DEG;
|
|
47
|
+
const lam = lonDeg * DEG;
|
|
48
|
+
const bet = latDeg * DEG;
|
|
49
|
+
const sinB = Math.sin(i) * Math.cos(bet) * Math.sin(lam - om)
|
|
50
|
+
- Math.cos(i) * Math.sin(bet);
|
|
51
|
+
const b = Math.abs(Math.asin(Math.max(-1.0, Math.min(1.0, sinB))));
|
|
52
|
+
return x - 8.914 - 1.825 * Math.sin(b) + 0.026 * a
|
|
53
|
+
- 0.378 * Math.sin(b) * Math.exp(-2.25 * a);
|
|
54
|
+
}
|
|
55
|
+
case "uranus":
|
|
56
|
+
// constant absorbs Mallama's sub-solar-latitude term
|
|
57
|
+
return x - 7.16 + 6.587e-3 * a + 1.045e-4 * a ** 2;
|
|
58
|
+
case "neptune": {
|
|
59
|
+
const y = 2000.0 + (jde - J2000) / 365.25;
|
|
60
|
+
const base = y < 1980.0 ? -6.89
|
|
61
|
+
: y < 2000.0 ? -6.89 - (0.11 * (y - 1980.0)) / 20.0
|
|
62
|
+
: -7.0;
|
|
63
|
+
return x + base + 7.944e-3 * a + 9.617e-5 * a ** 2;
|
|
64
|
+
}
|
|
65
|
+
default: // pluto
|
|
66
|
+
return x - 1.01;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Phase angle (deg), illuminated fraction, elongation (deg), apparent
|
|
70
|
+
* diameter (deg), apparent magnitude. */
|
|
71
|
+
export function pheno(engine, body, jdUt) {
|
|
72
|
+
if (DIAMETER_KM[body] === undefined) {
|
|
73
|
+
throw new Error(`pheno not available for '${body}'`);
|
|
74
|
+
}
|
|
75
|
+
const jde = jdTT(jdUt);
|
|
76
|
+
const p = engine.position(body, jdUt);
|
|
77
|
+
const s = body === "sun" ? p : engine.position("sun", jdUt);
|
|
78
|
+
const dlt = p.dist;
|
|
79
|
+
const a1 = p.lon * DEG;
|
|
80
|
+
const d1 = p.lat * DEG;
|
|
81
|
+
const a2 = s.lon * DEG;
|
|
82
|
+
const d2 = s.lat * DEG;
|
|
83
|
+
const elong = Math.acos(Math.max(-1.0, Math.min(1.0, Math.sin(d1) * Math.sin(d2) + Math.cos(d1) * Math.cos(d2) * Math.cos(a1 - a2))));
|
|
84
|
+
let phaseAngle;
|
|
85
|
+
let r;
|
|
86
|
+
if (body === "sun") {
|
|
87
|
+
phaseAngle = 0.0;
|
|
88
|
+
r = dlt;
|
|
89
|
+
}
|
|
90
|
+
else if (body === "moon") {
|
|
91
|
+
r = s.dist; // sun-earth distance stands in for sun-moon
|
|
92
|
+
const R = s.dist;
|
|
93
|
+
phaseAngle = Math.atan2(R * Math.sin(elong), dlt - R * Math.cos(elong));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
r = engine.heliocentric(body, jdUt).dist;
|
|
97
|
+
const cosi = (r * r + dlt * dlt - s.dist ** 2) / (2 * r * dlt);
|
|
98
|
+
phaseAngle = Math.acos(Math.max(-1.0, Math.min(1.0, cosi)));
|
|
99
|
+
}
|
|
100
|
+
const aDeg = phaseAngle / DEG;
|
|
101
|
+
const diam = (2 * Math.asin(DIAMETER_KM[body] / (2 * dlt * KM_PER_AU))) / DEG;
|
|
102
|
+
return {
|
|
103
|
+
phaseAngle: aDeg,
|
|
104
|
+
phase: (1 + Math.cos(phaseAngle)) / 2,
|
|
105
|
+
elongation: elong / DEG,
|
|
106
|
+
diameter: diam,
|
|
107
|
+
magnitude: magnitude(body, aDeg, r, dlt, jde, p.lon, p.lat),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Apparent minus mean solar time, minutes (Meeus ch. 28). */
|
|
111
|
+
export function equationOfTime(engine, jdUt) {
|
|
112
|
+
const jde = jdTT(jdUt);
|
|
113
|
+
const t = (jde - J2000) / 365250.0;
|
|
114
|
+
const l0 = mod(280.4664567 + 360007.6982779 * t + 0.03032028 * t * t
|
|
115
|
+
+ t ** 3 / 49931 - t ** 4 / 15300 - t ** 5 / 2000000, 360);
|
|
116
|
+
const [lon, lat] = sunApparent(engine.data, jde);
|
|
117
|
+
const [ra] = equatorial(lon, lat, trueObliquity(engine.data, jde));
|
|
118
|
+
const e = mod(l0 - 0.0057183 - ra / DEG + 180, 360) - 180;
|
|
119
|
+
return e * 4.0; // degrees -> minutes
|
|
120
|
+
}
|
|
121
|
+
/** Apparent ecliptic position -> azimuth (deg, from true north, east-
|
|
122
|
+
* positive) and true altitude (deg). No refraction. */
|
|
123
|
+
export function azAlt(data, lonDeg, latDeg, jdUt, obsLat, obsLonEast) {
|
|
124
|
+
const jde = jdTT(jdUt);
|
|
125
|
+
const eps = trueObliquity(data, jde);
|
|
126
|
+
const [ra, dec] = equatorial(lonDeg * DEG, latDeg * DEG, eps);
|
|
127
|
+
const lst = mod(gast(data, jdUt) + obsLonEast * DEG, TWO_PI);
|
|
128
|
+
const ha = lst - ra;
|
|
129
|
+
const phi = obsLat * DEG;
|
|
130
|
+
const alt = Math.asin(Math.sin(phi) * Math.sin(dec) + Math.cos(phi) * Math.cos(dec) * Math.cos(ha));
|
|
131
|
+
const azS = Math.atan2(Math.sin(ha), Math.cos(ha) * Math.sin(phi) - Math.tan(dec) * Math.cos(phi));
|
|
132
|
+
return [mod(azS / DEG + 180.0, 360.0), alt / DEG];
|
|
133
|
+
}
|
|
134
|
+
/** Saemundsson refraction, degrees. Returns the input unchanged when even
|
|
135
|
+
* the refracted altitude stays below the horizon (matches Swiss
|
|
136
|
+
* Ephemeris). */
|
|
137
|
+
export function refractTrueToApparent(altDeg, pressure = 1013.25, tempC = 15.0) {
|
|
138
|
+
if (altDeg < -2.0)
|
|
139
|
+
return altDeg;
|
|
140
|
+
let r = 1.02 / Math.tan((altDeg + 10.3 / (altDeg + 5.11)) * DEG);
|
|
141
|
+
r *= (pressure / 1010.0) * (283.0 / (273.0 + tempC));
|
|
142
|
+
const out = altDeg + r / 60.0;
|
|
143
|
+
return out < 0.0 ? altDeg : out;
|
|
144
|
+
}
|
|
145
|
+
/** Bennett refraction, degrees. */
|
|
146
|
+
export function refractApparentToTrue(altDeg, pressure = 1013.25, tempC = 15.0) {
|
|
147
|
+
if (altDeg < -2.0)
|
|
148
|
+
return altDeg;
|
|
149
|
+
let r = 1.0 / Math.tan((altDeg + 7.31 / (altDeg + 4.4)) * DEG);
|
|
150
|
+
r *= (pressure / 1010.0) * (283.0 / (273.0 + tempC));
|
|
151
|
+
return altDeg - r / 60.0;
|
|
152
|
+
}
|