caelus 0.2.1 → 0.3.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 +14 -3
- package/dist/src/chart.d.ts +55 -11
- package/dist/src/chart.js +198 -47
- package/dist/src/core.d.ts +20 -0
- package/dist/src/core.js +91 -22
- package/dist/src/houses.d.ts +31 -0
- package/dist/src/houses.js +205 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/pheno.d.ts +35 -0
- package/dist/src/pheno.js +152 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ ephemeris files. 1:1 port of the Python reference, checked by golden fixtures.
|
|
|
9
9
|
every planet ≤ 1″ (Sun–Saturn), Moon ≤ 2.5″, Chiron ≤ 1″, nodes ≤ 1″
|
|
10
10
|
(vs full DE431 files, 1850–2149), angles and Placidus cusps ≤ 3.2″ — all
|
|
11
11
|
invisible at the arcminute display precision chart software uses.
|
|
12
|
-
2. TypeScript port verified against Python golden fixtures: **
|
|
13
|
-
0 failures, worst deviation
|
|
12
|
+
2. TypeScript port verified against Python golden fixtures: **3,087 checks,
|
|
13
|
+
0 failures, worst deviation 1.64 nano-arcseconds.** The two implementations
|
|
14
14
|
are numerically identical.
|
|
15
15
|
|
|
16
16
|
Regenerate fixtures any time from the Python side; any future TS change must
|
package/accuracy.json
CHANGED
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
{ "name": "Mean node", "max": "0.1", "rms": "0.1", "note": "" },
|
|
19
19
|
{ "name": "True node", "max": "0.8", "rms": "0.4", "note": "vs full DE431 files; Swiss’s built-in Moshier mode itself differs from DE431 by up to ~15″ here" },
|
|
20
20
|
{ "name": "Ascendant / MC", "max": "3.2", "rms": "—", "note": "" },
|
|
21
|
-
{ "name": "Placidus cusps (all 12)", "max": "3.2", "rms": "—", "note": "" }
|
|
21
|
+
{ "name": "Placidus cusps (all 12)", "max": "3.2", "rms": "—", "note": "" },
|
|
22
|
+
{ "name": "Mean Lilith", "max": "1.3", "rms": "0.5", "note": "mean lunar apogee on the inclined orbit; latitude ≤0.1″" },
|
|
23
|
+
{ "name": "Sidereal longitudes", "max": "0.1", "rms": "—", "note": "ayanamsa model vs SE ≤0.30″ at the 1900/2099 edges (IAU 1976 vs Vondrák precession); Sun worst-case 0.08″ at 120 sampled epochs" },
|
|
24
|
+
{ "name": "RA / Dec", "max": "2.1", "rms": "—", "note": "rotation is exact; bound tracks each body's ecliptic accuracy (Moon worst)" },
|
|
25
|
+
{ "name": "Topocentric Moon", "max": "2.7", "rms": "—", "note": "parallax model adds ≤0.1″ over the geocentric bound" },
|
|
26
|
+
{ "name": "House cusps, 8 new systems", "max": "0.0", "rms": "0.0", "note": "Koch, Regiomontanus, Campanus, Alcabitius, Morinus, Meridian, Polich-Page, Vehlow: exact vs swe_houses_armc (200 polar-inclusive cases each)" },
|
|
27
|
+
{ "name": "Vertex / east point", "max": "0.0", "rms": "0.0", "note": "exact vs swe_houses_armc" },
|
|
28
|
+
{ "name": "Magnitudes", "max": "0.045 mag", "rms": "—", "note": "Mallama 2018; Moon (Allen law) valid to phase angle 140°" }
|
|
22
29
|
],
|
|
23
30
|
"summary": [
|
|
24
31
|
{ "label": "Sun–Saturn", "bound": "≤ 1″" },
|
|
@@ -27,6 +34,10 @@
|
|
|
27
34
|
{ "label": "Moon (series, embedded)", "bound": "≤ 10″" },
|
|
28
35
|
{ "label": "Pluto / Chiron", "bound": "≤ 2.5″ / ≤ 1″" },
|
|
29
36
|
{ "label": "Angles & Placidus cusps", "bound": "≤ 3.2″" },
|
|
30
|
-
{ "label": "True node", "bound": "≤ 1″" }
|
|
31
|
-
|
|
37
|
+
{ "label": "True node", "bound": "≤ 1″" },
|
|
38
|
+
{ "label": "Mean Lilith", "bound": "≤ 1.3″" },
|
|
39
|
+
{ "label": "Sidereal (5 ayanamsas)", "bound": "≤ 0.3″ added" },
|
|
40
|
+
{ "label": "8 new house systems", "bound": "exact (0.0″)" }
|
|
41
|
+
],
|
|
42
|
+
"v03_harness": "python/validate_swiss.py regenerates every figure above the line against pyswisseph 2.10 (Moshier mode)"
|
|
32
43
|
}
|
package/dist/src/chart.d.ts
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
/** astroengine chart -- public API: natal charts, aspects, retrogrades. */
|
|
2
|
-
import { EngineData } from "./core.js";
|
|
2
|
+
import { EngineData, AYANAMSA_J2000 } from "./core.js";
|
|
3
3
|
export declare const BODIES: readonly ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", "chiron", "mean_node", "true_node"];
|
|
4
4
|
export type Body = (typeof BODIES)[number];
|
|
5
|
+
/** Computable on request (not in the default chart set). */
|
|
6
|
+
export declare const EXTRA_BODIES: readonly ["mean_lilith"];
|
|
7
|
+
/** Core names keep autocomplete; any string id is accepted (data packs). */
|
|
8
|
+
export type BodyId = Body | (typeof EXTRA_BODIES)[number] | (string & {});
|
|
5
9
|
export declare const SIGNS: string[];
|
|
6
10
|
export declare const ASPECTS: Record<string, number>;
|
|
7
11
|
export declare const DEFAULT_ORBS: Record<string, number>;
|
|
8
|
-
export type HouseSystem = "placidus" | "porphyry" | "equal" | "whole_sign";
|
|
12
|
+
export type HouseSystem = "placidus" | "porphyry" | "equal" | "whole_sign" | "koch" | "regiomontanus" | "campanus" | "alcabitius" | "morinus" | "meridian" | "polich_page" | "vehlow";
|
|
13
|
+
export type Ayanamsa = keyof typeof AYANAMSA_J2000 & string;
|
|
14
|
+
export type Zodiac = "tropical" | `sidereal:${string}`;
|
|
15
|
+
export interface Observer {
|
|
16
|
+
lat: number;
|
|
17
|
+
lonEast: number;
|
|
18
|
+
altM?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface CalcOptions {
|
|
21
|
+
zodiac?: Zodiac;
|
|
22
|
+
topocentric?: boolean;
|
|
23
|
+
observer?: Observer;
|
|
24
|
+
}
|
|
25
|
+
export interface ChartOptions extends CalcOptions {
|
|
26
|
+
houseSystem?: HouseSystem;
|
|
27
|
+
bodies?: BodyId[];
|
|
28
|
+
orbs?: Record<string, number>;
|
|
29
|
+
}
|
|
9
30
|
export interface Position {
|
|
10
31
|
lon: number;
|
|
11
32
|
speed: number;
|
|
12
33
|
retrograde: boolean;
|
|
13
34
|
sign: string;
|
|
14
35
|
signDeg: number;
|
|
36
|
+
/** Ecliptic latitude, deg (0 for nodes). */
|
|
37
|
+
lat: number;
|
|
38
|
+
/** Geocentric distance in AU (Moon included); null for nodes and Lilith. */
|
|
39
|
+
dist: number | null;
|
|
40
|
+
/** Equatorial coordinates, true equinox of date, deg. */
|
|
41
|
+
ra: number;
|
|
42
|
+
dec: number;
|
|
15
43
|
}
|
|
16
44
|
export interface Aspect {
|
|
17
45
|
a: string;
|
|
@@ -21,30 +49,46 @@ export interface Aspect {
|
|
|
21
49
|
}
|
|
22
50
|
export interface Chart {
|
|
23
51
|
jdUt: number;
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
zodiac: Zodiac;
|
|
53
|
+
/** House system actually used. May differ from the request: Placidus and
|
|
54
|
+
* Koch are undefined above the polar circles and fall back to whole_sign. */
|
|
26
55
|
houseSystem: HouseSystem;
|
|
27
56
|
houseSystemRequested: HouseSystem;
|
|
28
57
|
bodies: Record<string, Position>;
|
|
29
58
|
angles: {
|
|
30
59
|
asc: number;
|
|
31
60
|
mc: number;
|
|
61
|
+
vertex: number;
|
|
62
|
+
eastPoint: number;
|
|
32
63
|
};
|
|
33
64
|
cusps: number[];
|
|
34
65
|
aspects: Aspect[];
|
|
35
66
|
}
|
|
36
67
|
export declare class Engine {
|
|
37
|
-
|
|
68
|
+
readonly data: EngineData;
|
|
38
69
|
private moonCheb;
|
|
39
70
|
private chironCheb;
|
|
40
71
|
constructor(data: EngineData);
|
|
41
72
|
private moonInRange;
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
/** Body ids this engine can compute, given the data it was handed. */
|
|
74
|
+
bodies(): BodyId[];
|
|
75
|
+
/** Apparent geocentric [lon rad, lat rad, dist AU | null]. */
|
|
76
|
+
private ecliptic;
|
|
77
|
+
private lonOnly;
|
|
78
|
+
/** Apparent geocentric ecliptic longitude (deg). Tropical: true equinox
|
|
79
|
+
* of date. Sidereal: mean equinox minus ayanamsa. */
|
|
80
|
+
longitude(body: BodyId, jdUt: number, opts?: CalcOptions): number;
|
|
81
|
+
/** Geometric heliocentric ecliptic of date (deg, deg, AU). */
|
|
82
|
+
heliocentric(body: BodyId, jdUt: number): {
|
|
83
|
+
lon: number;
|
|
84
|
+
lat: number;
|
|
85
|
+
dist: number;
|
|
86
|
+
};
|
|
87
|
+
/** Full position: lon/speed/retrograde/sign + lat, dist (AU), ra, dec. */
|
|
88
|
+
position(body: BodyId, jdUt: number, opts?: CalcOptions): Position;
|
|
89
|
+
/** Full natal chart. Time is UT. East longitude positive. The ninth
|
|
90
|
+
* argument takes a house system name (0.2.x form) or a ChartOptions bag. */
|
|
91
|
+
chart(y: number, mo: number, d: number, h: number, mi: number, s: number, lat: number, lonEast: number, opts?: HouseSystem | ChartOptions): Chart;
|
|
48
92
|
}
|
|
49
93
|
export declare function findAspects(bodies: Record<string, Position>, orbs?: Record<string, number>): Aspect[];
|
|
50
94
|
export declare function fmtLon(deg: number): string;
|
package/dist/src/chart.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/** astroengine chart -- public API: natal charts, aspects, retrogrades. */
|
|
2
|
-
import { DEG, mod, jdTT, julianDay, ChebSeries, planetApparent, sunApparent, moonApparentSeries, moonApparentPrecise, plutoApparent, chironApparent, meanNode, trueNodeSeries, trueNodePrecise, } from "./core.js";
|
|
2
|
+
import { DEG, mod, jdTT, julianDay, ChebSeries, planetApparent, sunApparent, moonApparentSeries, moonApparentPrecise, plutoApparent, chironApparent, meanNode, trueNodeSeries, trueNodePrecise, equatorial, ayanamsa, AYANAMSA_J2000, meanLilith, topocentricEcl, trueObliquity, nutation, plutoHeliocentric, vsopHeliocentric, precessEcliptic, J2000, } from "./core.js";
|
|
3
3
|
import * as H from "./houses.js";
|
|
4
|
+
const TWO_PI = 2 * Math.PI;
|
|
4
5
|
export const BODIES = [
|
|
5
6
|
"sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn",
|
|
6
7
|
"uranus", "neptune", "pluto", "chiron", "mean_node", "true_node",
|
|
7
8
|
];
|
|
9
|
+
/** Computable on request (not in the default chart set). */
|
|
10
|
+
export const EXTRA_BODIES = ["mean_lilith"];
|
|
11
|
+
/** Points: excluded from aspect search by default. */
|
|
12
|
+
const NOT_ASPECTABLE = new Set(["mean_node", "true_node", "mean_lilith"]);
|
|
8
13
|
export const SIGNS = [
|
|
9
14
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra",
|
|
10
15
|
"Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
|
@@ -15,6 +20,20 @@ export const ASPECTS = {
|
|
|
15
20
|
export const DEFAULT_ORBS = {
|
|
16
21
|
conjunction: 8, sextile: 4, square: 7, trine: 7, opposition: 8,
|
|
17
22
|
};
|
|
23
|
+
const KM_PER_AU = 149597870.7;
|
|
24
|
+
function parseZodiac(zodiac) {
|
|
25
|
+
if (zodiac === "tropical")
|
|
26
|
+
return null;
|
|
27
|
+
if (zodiac.startsWith("sidereal:")) {
|
|
28
|
+
const mode = zodiac.slice("sidereal:".length);
|
|
29
|
+
if (AYANAMSA_J2000[mode] !== undefined)
|
|
30
|
+
return mode;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`unknown zodiac ${JSON.stringify(zodiac)}`);
|
|
33
|
+
}
|
|
34
|
+
const VSOP_BODIES = new Set([
|
|
35
|
+
"mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune",
|
|
36
|
+
]);
|
|
18
37
|
export class Engine {
|
|
19
38
|
data;
|
|
20
39
|
moonCheb;
|
|
@@ -28,93 +47,225 @@ export class Engine {
|
|
|
28
47
|
return !!this.moonCheb
|
|
29
48
|
&& this.moonCheb.jd0 <= jde - 0.1 && jde + 0.1 <= this.moonCheb.jd1;
|
|
30
49
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
/** Body ids this engine can compute, given the data it was handed. */
|
|
51
|
+
bodies() {
|
|
52
|
+
return [...BODIES, ...EXTRA_BODIES].filter((b) => b !== "chiron" || this.chironCheb);
|
|
53
|
+
}
|
|
54
|
+
/** Apparent geocentric [lon rad, lat rad, dist AU | null]. */
|
|
55
|
+
ecliptic(body, jde) {
|
|
56
|
+
if (body === "sun")
|
|
57
|
+
return sunApparent(this.data, jde);
|
|
58
|
+
if (body === "moon") {
|
|
59
|
+
const [lon, lat, km] = this.moonInRange(jde)
|
|
40
60
|
? moonApparentPrecise(this.data, this.moonCheb, jde)
|
|
41
61
|
: moonApparentSeries(this.data, jde);
|
|
62
|
+
return [lon, lat, km / KM_PER_AU];
|
|
42
63
|
}
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
if (body === "pluto")
|
|
65
|
+
return plutoApparent(this.data, jde);
|
|
66
|
+
if (body === "chiron") {
|
|
67
|
+
if (!this.chironCheb)
|
|
68
|
+
throw new Error("chiron data not loaded");
|
|
69
|
+
return chironApparent(this.data, this.chironCheb, jde);
|
|
70
|
+
}
|
|
71
|
+
if (body === "mean_node")
|
|
72
|
+
return [meanNode(this.data, jde), 0.0, null];
|
|
73
|
+
if (body === "true_node") {
|
|
74
|
+
return [
|
|
75
|
+
this.moonInRange(jde)
|
|
76
|
+
? trueNodePrecise(this.data, this.moonCheb, jde)
|
|
77
|
+
: trueNodeSeries(this.data, jde),
|
|
78
|
+
0.0, null,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
if (body === "mean_lilith") {
|
|
82
|
+
const [lon, lat] = meanLilith(this.data, jde);
|
|
83
|
+
return [lon, lat, null];
|
|
84
|
+
}
|
|
85
|
+
if (this.data.vsop[body])
|
|
86
|
+
return planetApparent(this.data, body, jde);
|
|
87
|
+
throw new Error(`no data loaded for body '${body}'`);
|
|
88
|
+
}
|
|
89
|
+
lonOnly(body, jdUt, mode, topo) {
|
|
90
|
+
const jde = jdTT(jdUt);
|
|
91
|
+
let [lon, lat, dist] = this.ecliptic(body, jde);
|
|
92
|
+
if (topo !== null && dist !== null) {
|
|
93
|
+
const lst = mod(H.gast(this.data, jdUt) + topo.lonEast * DEG, TWO_PI);
|
|
94
|
+
[lon, lat, dist] = topocentricEcl(lon, lat, dist, lst, topo.lat * DEG, topo.altM ?? 0.0, trueObliquity(this.data, jde));
|
|
95
|
+
}
|
|
96
|
+
let lonDeg = lon / DEG;
|
|
97
|
+
if (mode !== null) {
|
|
98
|
+
lonDeg = mod(lonDeg - nutation(this.data, jde)[0] / DEG - ayanamsa(jde, mode), 360);
|
|
99
|
+
}
|
|
100
|
+
return lonDeg;
|
|
101
|
+
}
|
|
102
|
+
/** Apparent geocentric ecliptic longitude (deg). Tropical: true equinox
|
|
103
|
+
* of date. Sidereal: mean equinox minus ayanamsa. */
|
|
104
|
+
longitude(body, jdUt, opts = {}) {
|
|
105
|
+
const mode = parseZodiac(opts.zodiac ?? "tropical");
|
|
106
|
+
const topo = opts.topocentric ? opts.observer ?? null : null;
|
|
107
|
+
return this.lonOnly(body, jdUt, mode, topo);
|
|
108
|
+
}
|
|
109
|
+
/** Geometric heliocentric ecliptic of date (deg, deg, AU). */
|
|
110
|
+
heliocentric(body, jdUt) {
|
|
111
|
+
const jde = jdTT(jdUt);
|
|
112
|
+
let l;
|
|
113
|
+
let b;
|
|
114
|
+
let r;
|
|
115
|
+
if (body === "pluto") {
|
|
116
|
+
[l, b, r] = plutoHeliocentric(this.data, jde);
|
|
117
|
+
[l, b] = precessEcliptic(l, b, J2000, jde);
|
|
45
118
|
}
|
|
46
119
|
else if (body === "chiron") {
|
|
47
120
|
if (!this.chironCheb)
|
|
48
121
|
throw new Error("chiron data not loaded");
|
|
49
|
-
[
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
122
|
+
const [x, y, z] = this.chironCheb.xyz(jde);
|
|
123
|
+
r = Math.sqrt(x * x + y * y + z * z);
|
|
124
|
+
l = mod(Math.atan2(y, x), TWO_PI);
|
|
125
|
+
b = Math.atan2(z, Math.hypot(x, y));
|
|
126
|
+
[l, b] = precessEcliptic(l, b, J2000, jde);
|
|
53
127
|
}
|
|
54
|
-
else if (body
|
|
55
|
-
|
|
56
|
-
? trueNodePrecise(this.data, this.moonCheb, jde)
|
|
57
|
-
: trueNodeSeries(this.data, jde);
|
|
128
|
+
else if (VSOP_BODIES.has(body) && this.data.vsop[body]) {
|
|
129
|
+
[l, b, r] = vsopHeliocentric(this.data.vsop[body], jde);
|
|
58
130
|
}
|
|
59
131
|
else {
|
|
60
|
-
|
|
132
|
+
throw new Error(`no heliocentric position for '${body}'`);
|
|
61
133
|
}
|
|
62
|
-
return lon / DEG;
|
|
134
|
+
return { lon: l / DEG, lat: b / DEG, dist: r };
|
|
63
135
|
}
|
|
64
|
-
/**
|
|
65
|
-
position(body, jdUt) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
136
|
+
/** Full position: lon/speed/retrograde/sign + lat, dist (AU), ra, dec. */
|
|
137
|
+
position(body, jdUt, opts = {}) {
|
|
138
|
+
const mode = parseZodiac(opts.zodiac ?? "tropical");
|
|
139
|
+
const topo = opts.topocentric ? opts.observer ?? null : null;
|
|
140
|
+
const jde = jdTT(jdUt);
|
|
141
|
+
let [lonR, latR, dist] = this.ecliptic(body, jde);
|
|
142
|
+
if (topo !== null && dist !== null) {
|
|
143
|
+
const lst = mod(H.gast(this.data, jdUt) + topo.lonEast * DEG, TWO_PI);
|
|
144
|
+
[lonR, latR, dist] = topocentricEcl(lonR, latR, dist, lst, topo.lat * DEG, topo.altM ?? 0.0, trueObliquity(this.data, jde));
|
|
145
|
+
}
|
|
146
|
+
const [ra, dec] = equatorial(lonR, latR, trueObliquity(this.data, jde));
|
|
147
|
+
let lon = lonR / DEG;
|
|
148
|
+
if (mode !== null) {
|
|
149
|
+
lon = mod(lon - nutation(this.data, jde)[0] / DEG - ayanamsa(jde, mode), 360);
|
|
150
|
+
}
|
|
151
|
+
const h = 0.25; // days; central difference
|
|
152
|
+
const l0 = this.lonOnly(body, jdUt - h, mode, topo);
|
|
153
|
+
const l1 = this.lonOnly(body, jdUt + h, mode, topo);
|
|
70
154
|
const speed = (mod(l1 - l0 + 540, 360) - 180) / (2 * h);
|
|
71
155
|
return {
|
|
72
156
|
lon, speed, retrograde: speed < 0,
|
|
73
157
|
sign: SIGNS[Math.floor(lon / 30)], signDeg: mod(lon, 30),
|
|
158
|
+
lat: latR / DEG, dist,
|
|
159
|
+
ra: ra / DEG, dec: dec / DEG,
|
|
74
160
|
};
|
|
75
161
|
}
|
|
76
|
-
/** Full natal chart. Time is UT. East longitude positive.
|
|
77
|
-
|
|
162
|
+
/** Full natal chart. Time is UT. East longitude positive. The ninth
|
|
163
|
+
* argument takes a house system name (0.2.x form) or a ChartOptions bag. */
|
|
164
|
+
chart(y, mo, d, h, mi, s, lat, lonEast, opts = "placidus") {
|
|
165
|
+
const o = typeof opts === "string" ? { houseSystem: opts } : opts;
|
|
166
|
+
const houseSystem = o.houseSystem ?? "placidus";
|
|
167
|
+
const zodiac = o.zodiac ?? "tropical";
|
|
168
|
+
const mode = parseZodiac(zodiac);
|
|
78
169
|
const jdUt = julianDay(y, mo, d, h, mi, s);
|
|
170
|
+
const calc = {
|
|
171
|
+
zodiac,
|
|
172
|
+
topocentric: o.topocentric,
|
|
173
|
+
observer: o.topocentric ? o.observer ?? { lat, lonEast, altM: 0.0 } : undefined,
|
|
174
|
+
};
|
|
175
|
+
const names = [
|
|
176
|
+
...BODIES, ...(o.bodies ?? []).filter((b) => !BODIES.includes(b)),
|
|
177
|
+
];
|
|
79
178
|
const bodies = {};
|
|
80
|
-
for (const b of
|
|
81
|
-
bodies[b] = this.position(b, jdUt);
|
|
179
|
+
for (const b of names)
|
|
180
|
+
bodies[b] = this.position(b, jdUt, calc);
|
|
82
181
|
const [asc, mc, armc, eps] = H.angles(this.data, jdUt, lat, lonEast);
|
|
182
|
+
const [vtx, east] = H.vertexEastPoint(armc, lat * DEG, eps);
|
|
83
183
|
const phi = lat * DEG;
|
|
84
|
-
let cusps;
|
|
85
184
|
let used = houseSystem;
|
|
86
|
-
|
|
87
|
-
|
|
185
|
+
let cusps;
|
|
186
|
+
try {
|
|
187
|
+
if (houseSystem === "placidus") {
|
|
188
|
+
if (Math.abs(lat) >= 66.0) {
|
|
189
|
+
throw new RangeError("placidus undefined above polar circles");
|
|
190
|
+
}
|
|
88
191
|
cusps = H.housesPlacidus(armc, phi, eps);
|
|
89
192
|
}
|
|
90
|
-
else {
|
|
91
|
-
|
|
193
|
+
else if (houseSystem === "porphyry") {
|
|
194
|
+
cusps = H.housesPorphyry(asc, mc);
|
|
195
|
+
}
|
|
196
|
+
else if (houseSystem === "equal") {
|
|
197
|
+
cusps = H.housesEqual(asc);
|
|
198
|
+
}
|
|
199
|
+
else if (houseSystem === "whole_sign") {
|
|
92
200
|
cusps = H.housesWholeSign(asc);
|
|
93
201
|
}
|
|
202
|
+
else if (houseSystem === "koch") {
|
|
203
|
+
cusps = H.housesKoch(armc, phi, eps);
|
|
204
|
+
}
|
|
205
|
+
else if (houseSystem === "regiomontanus") {
|
|
206
|
+
cusps = H.housesRegiomontanus(armc, phi, eps);
|
|
207
|
+
}
|
|
208
|
+
else if (houseSystem === "campanus") {
|
|
209
|
+
cusps = H.housesCampanus(armc, phi, eps);
|
|
210
|
+
}
|
|
211
|
+
else if (houseSystem === "alcabitius") {
|
|
212
|
+
cusps = H.housesAlcabitius(armc, phi, eps);
|
|
213
|
+
}
|
|
214
|
+
else if (houseSystem === "morinus") {
|
|
215
|
+
cusps = H.housesMorinus(armc, phi, eps);
|
|
216
|
+
}
|
|
217
|
+
else if (houseSystem === "meridian") {
|
|
218
|
+
cusps = H.housesMeridian(armc, phi, eps);
|
|
219
|
+
}
|
|
220
|
+
else if (houseSystem === "polich_page") {
|
|
221
|
+
cusps = H.housesPolichPage(armc, phi, eps);
|
|
222
|
+
}
|
|
223
|
+
else if (houseSystem === "vehlow") {
|
|
224
|
+
cusps = H.housesVehlow(armc, phi, eps);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
throw new Error(`unknown house system '${houseSystem}'`);
|
|
228
|
+
}
|
|
94
229
|
}
|
|
95
|
-
|
|
96
|
-
|
|
230
|
+
catch (err) {
|
|
231
|
+
if (!(err instanceof RangeError))
|
|
232
|
+
throw err;
|
|
233
|
+
used = "whole_sign"; // Placidus/Koch undefined above polar circles
|
|
234
|
+
cusps = H.housesWholeSign(asc);
|
|
97
235
|
}
|
|
98
|
-
|
|
99
|
-
|
|
236
|
+
const jde = jdTT(jdUt);
|
|
237
|
+
let shift = 0.0;
|
|
238
|
+
if (mode !== null) {
|
|
239
|
+
shift = nutation(this.data, jde)[0] / DEG + ayanamsa(jde, mode);
|
|
240
|
+
}
|
|
241
|
+
const outDeg = (rad) => mod(rad / DEG - shift, 360);
|
|
242
|
+
let cuspsDeg;
|
|
243
|
+
if (mode !== null && used === "whole_sign") {
|
|
244
|
+
// whole-sign cusps must stay sign-aligned in the sidereal zodiac
|
|
245
|
+
const first = Math.floor(outDeg(asc) / 30) * 30.0;
|
|
246
|
+
cuspsDeg = Array.from({ length: 12 }, (_, i) => mod(first + i * 30.0, 360));
|
|
100
247
|
}
|
|
101
248
|
else {
|
|
102
|
-
|
|
249
|
+
cuspsDeg = cusps.map(outDeg);
|
|
103
250
|
}
|
|
104
251
|
return {
|
|
105
252
|
jdUt,
|
|
253
|
+
zodiac,
|
|
106
254
|
houseSystem: used,
|
|
107
255
|
houseSystemRequested: houseSystem,
|
|
108
256
|
bodies,
|
|
109
|
-
angles: {
|
|
110
|
-
|
|
111
|
-
|
|
257
|
+
angles: {
|
|
258
|
+
asc: outDeg(asc), mc: outDeg(mc),
|
|
259
|
+
vertex: outDeg(vtx), eastPoint: outDeg(east),
|
|
260
|
+
},
|
|
261
|
+
cusps: cuspsDeg,
|
|
262
|
+
aspects: findAspects(bodies, o.orbs ?? DEFAULT_ORBS),
|
|
112
263
|
};
|
|
113
264
|
}
|
|
114
265
|
}
|
|
115
266
|
export function findAspects(bodies, orbs = DEFAULT_ORBS) {
|
|
116
267
|
const out = [];
|
|
117
|
-
const names = Object.keys(bodies).filter((b) => !
|
|
268
|
+
const names = Object.keys(bodies).filter((b) => !NOT_ASPECTABLE.has(b));
|
|
118
269
|
for (let i = 0; i < names.length; i++) {
|
|
119
270
|
for (let j = i + 1; j < names.length; j++) {
|
|
120
271
|
const a = names[i];
|
|
@@ -134,5 +285,5 @@ export function fmtLon(deg) {
|
|
|
134
285
|
const sign = SIGNS[Math.floor(deg / 30)];
|
|
135
286
|
const d = mod(deg, 30);
|
|
136
287
|
const m = mod(d, 1) * 60;
|
|
137
|
-
return `${String(Math.floor(d)).padStart(2)}
|
|
288
|
+
return `${String(Math.floor(d)).padStart(2)}°${String(Math.floor(m)).padStart(2, "0")}' ${sign}`;
|
|
138
289
|
}
|
package/dist/src/core.d.ts
CHANGED
|
@@ -68,5 +68,25 @@ export declare function trueNodePrecise(data: EngineData, cheb: ChebSeries, jde:
|
|
|
68
68
|
export declare function meanNode(data: EngineData, jde: number): number;
|
|
69
69
|
/** Osculating node from the series moon (fallback outside Chebyshev range). */
|
|
70
70
|
export declare function trueNodeSeries(data: EngineData, jde: number): number;
|
|
71
|
+
/** Ecliptic lon/lat -> right ascension, declination (all radians). */
|
|
72
|
+
export declare function equatorial(lon: number, lat: number, eps: number): [number, number];
|
|
73
|
+
/** Mean ayanamsa at J2000.0 (degrees) per mode. Standard epoch anchors
|
|
74
|
+
* (matched to Swiss Ephemeris 2.10 to 1e-9 deg); propagation uses IAU 1976
|
|
75
|
+
* ecliptic precession. Agreement with Swiss Ephemeris over 1900-2099 is
|
|
76
|
+
* <=0.30 arcsec (precession-model difference: SE uses Vondrak 2011). */
|
|
77
|
+
export declare const AYANAMSA_J2000: Record<string, number>;
|
|
78
|
+
/** Mean ayanamsa in degrees. Sidereal longitude = (tropical true-equinox
|
|
79
|
+
* longitude - nutation in longitude) - ayanamsa: the sidereal zodiac is
|
|
80
|
+
* anchored to the mean equinox. */
|
|
81
|
+
export declare function ayanamsa(jde: number, mode: string): number;
|
|
82
|
+
/** Mean lunar apogee (Black Moon Lilith) on the inclined lunar orbit:
|
|
83
|
+
* apparent lon (true equinox) and orbital latitude, radians. */
|
|
84
|
+
export declare function meanLilith(data: EngineData, jde: number): [number, number];
|
|
85
|
+
export declare const EARTH_RADIUS_AU: number;
|
|
86
|
+
/** Diurnal parallax in ecliptic coordinates (Meeus ch. 11/40).
|
|
87
|
+
* lst = local apparent sidereal time (rad). Returns [lon, lat, distAu]. */
|
|
88
|
+
export declare function topocentricEcl(lon: number, lat: number, distAu: number, lst: number, obsLat: number, altM: number, eps: number): [number, number, number];
|
|
89
|
+
/** Meeus ch.37 heliocentric Pluto, ecliptic J2000: [l rad, b rad, r AU]. */
|
|
90
|
+
export declare function plutoHeliocentric(data: EngineData, jde: number): [number, number, number];
|
|
71
91
|
export declare function plutoApparent(data: EngineData, jde: number): [number, number, number];
|
|
72
92
|
export declare function chironApparent(data: EngineData, cheb: ChebSeries, jde: number): [number, number, number];
|
package/dist/src/core.js
CHANGED
|
@@ -377,30 +377,99 @@ export function trueNodeSeries(data, jde) {
|
|
|
377
377
|
const node = mod(Math.atan2(hx, -hy), TWO_PI);
|
|
378
378
|
return mod(node + nutation(data, jde)[0], TWO_PI);
|
|
379
379
|
}
|
|
380
|
+
// ---------------------------------------------------------------- frames+
|
|
381
|
+
/** Ecliptic lon/lat -> right ascension, declination (all radians). */
|
|
382
|
+
export function equatorial(lon, lat, eps) {
|
|
383
|
+
const ra = mod(Math.atan2(Math.sin(lon) * Math.cos(eps) - Math.tan(lat) * Math.sin(eps), Math.cos(lon)), TWO_PI);
|
|
384
|
+
const dec = Math.asin(Math.sin(lat) * Math.cos(eps) + Math.cos(lat) * Math.sin(eps) * Math.sin(lon));
|
|
385
|
+
return [ra, dec];
|
|
386
|
+
}
|
|
387
|
+
/** Mean ayanamsa at J2000.0 (degrees) per mode. Standard epoch anchors
|
|
388
|
+
* (matched to Swiss Ephemeris 2.10 to 1e-9 deg); propagation uses IAU 1976
|
|
389
|
+
* ecliptic precession. Agreement with Swiss Ephemeris over 1900-2099 is
|
|
390
|
+
* <=0.30 arcsec (precession-model difference: SE uses Vondrak 2011). */
|
|
391
|
+
export const AYANAMSA_J2000 = {
|
|
392
|
+
lahiri: 23.857092325,
|
|
393
|
+
fagan_bradley: 24.740299966,
|
|
394
|
+
krishnamurti: 23.760240012,
|
|
395
|
+
raman: 22.410791012,
|
|
396
|
+
yukteshwar: 22.478803000,
|
|
397
|
+
};
|
|
398
|
+
/** Mean ayanamsa in degrees. Sidereal longitude = (tropical true-equinox
|
|
399
|
+
* longitude - nutation in longitude) - ayanamsa: the sidereal zodiac is
|
|
400
|
+
* anchored to the mean equinox. */
|
|
401
|
+
export function ayanamsa(jde, mode) {
|
|
402
|
+
const a0 = AYANAMSA_J2000[mode];
|
|
403
|
+
if (a0 === undefined)
|
|
404
|
+
throw new Error(`unknown ayanamsa ${mode}`);
|
|
405
|
+
const [lon] = precessEcliptic(a0 * DEG, 0.0, J2000, jde);
|
|
406
|
+
return lon / DEG;
|
|
407
|
+
}
|
|
408
|
+
/** Mean lunar apogee (Black Moon Lilith) on the inclined lunar orbit:
|
|
409
|
+
* apparent lon (true equinox) and orbital latitude, radians. */
|
|
410
|
+
export function meanLilith(data, jde) {
|
|
411
|
+
const T = (jde - J2000) / 36525.0;
|
|
412
|
+
const [Lp, , , Mp] = moonFundamental(T);
|
|
413
|
+
const apog = Lp - Mp + Math.PI; // mean perigee + 180
|
|
414
|
+
const om = (125.0445479 - 1934.1362891 * T + 0.0020754 * T * T
|
|
415
|
+
+ T ** 3 / 467441 - T ** 4 / 60616000) * DEG;
|
|
416
|
+
const inc = 5.145396374 * DEG;
|
|
417
|
+
const u = apog - om;
|
|
418
|
+
const lat = Math.asin(Math.sin(inc) * Math.sin(u));
|
|
419
|
+
let lon = om + Math.atan2(Math.cos(inc) * Math.sin(u), Math.cos(u));
|
|
420
|
+
lon = mod(lon + nutation(data, jde)[0], TWO_PI);
|
|
421
|
+
return [lon, lat];
|
|
422
|
+
}
|
|
423
|
+
export const EARTH_RADIUS_AU = 6378.14 / 149597870.7;
|
|
424
|
+
const EARTH_FLAT = 0.99664719; // 1 - f, IAU 1976 figure
|
|
425
|
+
/** Diurnal parallax in ecliptic coordinates (Meeus ch. 11/40).
|
|
426
|
+
* lst = local apparent sidereal time (rad). Returns [lon, lat, distAu]. */
|
|
427
|
+
export function topocentricEcl(lon, lat, distAu, lst, obsLat, altM, eps) {
|
|
428
|
+
const u = Math.atan(EARTH_FLAT * Math.tan(obsLat));
|
|
429
|
+
const rs = EARTH_FLAT * Math.sin(u) + (altM / 6378140.0) * Math.sin(obsLat);
|
|
430
|
+
const rc = Math.cos(u) + (altM / 6378140.0) * Math.cos(obsLat);
|
|
431
|
+
const ox = EARTH_RADIUS_AU * rc * Math.cos(lst);
|
|
432
|
+
const oy = EARTH_RADIUS_AU * rc * Math.sin(lst);
|
|
433
|
+
const oz = EARTH_RADIUS_AU * rs;
|
|
434
|
+
const [ra, dec] = equatorial(lon, lat, eps);
|
|
435
|
+
const bx = distAu * Math.cos(dec) * Math.cos(ra);
|
|
436
|
+
const by = distAu * Math.cos(dec) * Math.sin(ra);
|
|
437
|
+
const bz = distAu * Math.sin(dec);
|
|
438
|
+
const tx = bx - ox;
|
|
439
|
+
const ty = by - oy;
|
|
440
|
+
const tz = bz - oz;
|
|
441
|
+
const ra2 = Math.atan2(ty, tx);
|
|
442
|
+
const dec2 = Math.atan2(tz, Math.hypot(tx, ty));
|
|
443
|
+
const lon2 = mod(Math.atan2(Math.sin(ra2) * Math.cos(eps) + Math.tan(dec2) * Math.sin(eps), Math.cos(ra2)), TWO_PI);
|
|
444
|
+
const lat2 = Math.asin(Math.sin(dec2) * Math.cos(eps) - Math.cos(dec2) * Math.sin(eps) * Math.sin(ra2));
|
|
445
|
+
return [lon2, lat2, Math.sqrt(tx * tx + ty * ty + tz * tz)];
|
|
446
|
+
}
|
|
380
447
|
// ---------------------------------------------------------------- pluto
|
|
448
|
+
/** Meeus ch.37 heliocentric Pluto, ecliptic J2000: [l rad, b rad, r AU]. */
|
|
449
|
+
export function plutoHeliocentric(data, jde) {
|
|
450
|
+
const T = (jde - J2000) / 36525.0;
|
|
451
|
+
const J = (34.35 + 3034.9057 * T) * DEG;
|
|
452
|
+
const S = (50.08 + 1222.1138 * T) * DEG;
|
|
453
|
+
const P = (238.96 + 144.96 * T) * DEG;
|
|
454
|
+
let l = 0.0;
|
|
455
|
+
let b = 0.0;
|
|
456
|
+
let r = 0.0;
|
|
457
|
+
for (const [i, j, k, lA, lB, bA, bB, rA, rB] of data.pluto) {
|
|
458
|
+
const a = i * J + j * S + k * P;
|
|
459
|
+
const sa = Math.sin(a);
|
|
460
|
+
const ca = Math.cos(a);
|
|
461
|
+
l += lA * sa + lB * ca;
|
|
462
|
+
b += bA * sa + bB * ca;
|
|
463
|
+
r += rA * sa + rB * ca;
|
|
464
|
+
}
|
|
465
|
+
return [
|
|
466
|
+
(l + 238.958116 + 144.96 * T) * DEG,
|
|
467
|
+
(b - 3.908239) * DEG,
|
|
468
|
+
r + 40.7241346,
|
|
469
|
+
];
|
|
470
|
+
}
|
|
381
471
|
export function plutoApparent(data, jde) {
|
|
382
|
-
const helioJ2000 = (tJde) =>
|
|
383
|
-
const T = (tJde - J2000) / 36525.0;
|
|
384
|
-
const J = (34.35 + 3034.9057 * T) * DEG;
|
|
385
|
-
const S = (50.08 + 1222.1138 * T) * DEG;
|
|
386
|
-
const P = (238.96 + 144.96 * T) * DEG;
|
|
387
|
-
let l = 0.0;
|
|
388
|
-
let b = 0.0;
|
|
389
|
-
let r = 0.0;
|
|
390
|
-
for (const [i, j, k, lA, lB, bA, bB, rA, rB] of data.pluto) {
|
|
391
|
-
const a = i * J + j * S + k * P;
|
|
392
|
-
const sa = Math.sin(a);
|
|
393
|
-
const ca = Math.cos(a);
|
|
394
|
-
l += lA * sa + lB * ca;
|
|
395
|
-
b += bA * sa + bB * ca;
|
|
396
|
-
r += rA * sa + rB * ca;
|
|
397
|
-
}
|
|
398
|
-
return [
|
|
399
|
-
(l + 238.958116 + 144.96 * T) * DEG,
|
|
400
|
-
(b - 3.908239) * DEG,
|
|
401
|
-
r + 40.7241346,
|
|
402
|
-
];
|
|
403
|
-
};
|
|
472
|
+
const helioJ2000 = (tJde) => plutoHeliocentric(data, tJde);
|
|
404
473
|
const [L0d, B0d, R0d] = vsopHeliocentric(data.vsop.earth, jde);
|
|
405
474
|
const [Lj, Bj] = precessEcliptic(L0d, B0d, jde, J2000);
|
|
406
475
|
const ex = R0d * Math.cos(Bj) * Math.cos(Lj);
|
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
|
@@ -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
|
+
}
|