rave-engine 1.0.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/LICENSE +21 -0
- package/NOTICE +33 -0
- package/README.md +354 -0
- package/bin/rave.js +204 -0
- package/package.json +68 -0
- package/src/birth/parseBirth.js +76 -0
- package/src/calc/astronomia.js +163 -0
- package/src/calc/ephemeris.js +61 -0
- package/src/calc/mandala.js +66 -0
- package/src/calc/profile.js +288 -0
- package/src/calc/swisseph.js +87 -0
- package/src/chart.js +84 -0
- package/src/hd/bodygraph.js +270 -0
- package/src/hd/houses.js +229 -0
- package/src/index.js +89 -0
- package/src/timezone/location.js +67 -0
- package/src/timezone/search.js +183 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { DateTime } = require('luxon');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BirthParseError carries an HTTP-ish status code so a thin adapter (e.g. an
|
|
5
|
+
* Express handler) can map it back to a 400 response. We keep a plain class
|
|
6
|
+
* here so the engine doesn't need to know about Express.
|
|
7
|
+
*/
|
|
8
|
+
class BirthParseError extends Error {
|
|
9
|
+
constructor(message, statusCode = 400) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'BirthParseError';
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseYmdLoose(v) {
|
|
17
|
+
// Accept both ISO-padded and non-padded dates:
|
|
18
|
+
// - YYYY-MM-DD
|
|
19
|
+
// - YYYY-M-D
|
|
20
|
+
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(v || '').trim());
|
|
21
|
+
if (!m) return null;
|
|
22
|
+
const year = Number(m[1]);
|
|
23
|
+
const month = Number(m[2]);
|
|
24
|
+
const day = Number(m[3]);
|
|
25
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return null;
|
|
26
|
+
if (month < 1 || month > 12) return null;
|
|
27
|
+
if (day < 1 || day > 31) return null;
|
|
28
|
+
return { year, month, day };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseHmsLoose(v) {
|
|
32
|
+
// Accept:
|
|
33
|
+
// - HH:mm
|
|
34
|
+
// - H:mm
|
|
35
|
+
// - HH:mm:ss
|
|
36
|
+
const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(String(v || '').trim());
|
|
37
|
+
if (!m) return null;
|
|
38
|
+
const hour = Number(m[1]);
|
|
39
|
+
const minute = Number(m[2]);
|
|
40
|
+
const second = m[3] === undefined ? 0 : Number(m[3]);
|
|
41
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(second)) return null;
|
|
42
|
+
if (hour < 0 || hour > 23) return null;
|
|
43
|
+
if (minute < 0 || minute > 59) return null;
|
|
44
|
+
if (second < 0 || second > 59) return null;
|
|
45
|
+
return { hour, minute, second };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseBirthToUtc({ birthdate, birthtime, timezone }) {
|
|
49
|
+
if (!birthdate || !birthtime || !timezone) {
|
|
50
|
+
throw new BirthParseError('manual mode requires birthdate, birthtime, timezone');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// birthdate: YYYY-MM-DD (or YYYY-M-D), birthtime: HH:mm (or H:mm) or HH:mm:ss
|
|
54
|
+
//
|
|
55
|
+
// NOTE:
|
|
56
|
+
// Luxon `DateTime.fromISO()` is strict about ISO padding (e.g. it rejects "1972-8-2").
|
|
57
|
+
// We normalize by parsing components and building a DateTime from an object instead.
|
|
58
|
+
const dateParts = parseYmdLoose(birthdate);
|
|
59
|
+
const timeParts = parseHmsLoose(birthtime);
|
|
60
|
+
if (!dateParts || !timeParts) {
|
|
61
|
+
throw new BirthParseError(
|
|
62
|
+
'Invalid birthdate/birthtime format. Expected birthdate "YYYY-MM-DD" (or "YYYY-M-D") and birthtime "HH:mm" (or "H:mm") optionally with seconds "HH:mm:ss".'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dt = DateTime.fromObject({ ...dateParts, ...timeParts }, { zone: timezone });
|
|
67
|
+
if (!dt.isValid) {
|
|
68
|
+
throw new BirthParseError(
|
|
69
|
+
`Invalid datetime/timezone: ${dt.invalidReason || 'unknown'} (birthdate=${birthdate}, birthtime=${birthtime}, timezone=${timezone})`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return dt.toUTC().toJSDate();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { parseBirthToUtc, BirthParseError };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Pure-JS ephemeris backend, built on the MIT-licensed `astronomia` package.
|
|
2
|
+
//
|
|
3
|
+
// This is the DEFAULT backend for rave-engine: no native build, no `.se1` data
|
|
4
|
+
// files, no AGPL. It reproduces the same apparent geocentric tropical ecliptic
|
|
5
|
+
// longitudes that the Swiss Ephemeris (`SEFLG_SWIEPH`) produces — validated to
|
|
6
|
+
// the exact gate/line in the golden vectors (see test/profile.test.js).
|
|
7
|
+
//
|
|
8
|
+
// All longitudes are returned in DEGREES in [0, 360).
|
|
9
|
+
//
|
|
10
|
+
// Correctness note (the #1 trap): Swiss Ephemeris `swe_calc_ut` takes UT and
|
|
11
|
+
// adds ΔT internally; astronomia's planet/Sun/Moon functions take JDE = UT + ΔT.
|
|
12
|
+
// We convert UT → JDE once per call via `deltat.deltaT`, and keep all Julian
|
|
13
|
+
// Days the caller sees in UT (the engine's `_meta` JDs are UT).
|
|
14
|
+
|
|
15
|
+
const A = require('astronomia');
|
|
16
|
+
const data = require('astronomia/data').default;
|
|
17
|
+
|
|
18
|
+
const { normalizeAngleDegrees } = require('./mandala');
|
|
19
|
+
|
|
20
|
+
const R2D = 180 / Math.PI;
|
|
21
|
+
|
|
22
|
+
// VSOP87 planet instances (heliocentric spherical, "B" series). Built once.
|
|
23
|
+
const EARTH = new A.planetposition.Planet(data.vsop87Bearth);
|
|
24
|
+
const PLANETS = {
|
|
25
|
+
mercury: new A.planetposition.Planet(data.vsop87Bmercury),
|
|
26
|
+
venus: new A.planetposition.Planet(data.vsop87Bvenus),
|
|
27
|
+
mars: new A.planetposition.Planet(data.vsop87Bmars),
|
|
28
|
+
jupiter: new A.planetposition.Planet(data.vsop87Bjupiter),
|
|
29
|
+
saturn: new A.planetposition.Planet(data.vsop87Bsaturn),
|
|
30
|
+
uranus: new A.planetposition.Planet(data.vsop87Buranus),
|
|
31
|
+
neptune: new A.planetposition.Planet(data.vsop87Bneptune),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Bodies this backend can place directly. Earth, south_node are DERIVED by the
|
|
35
|
+
// caller (Earth = Sun + 180°, South Node = North Node + 180°).
|
|
36
|
+
const BODIES = [
|
|
37
|
+
'sun',
|
|
38
|
+
'moon',
|
|
39
|
+
'mercury',
|
|
40
|
+
'venus',
|
|
41
|
+
'mars',
|
|
42
|
+
'jupiter',
|
|
43
|
+
'saturn',
|
|
44
|
+
'uranus',
|
|
45
|
+
'neptune',
|
|
46
|
+
'pluto',
|
|
47
|
+
'north_node',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Julian Day (UT) from a JS Date. Mirrors swe.swe_julday(..., SE_GREG_CAL).
|
|
52
|
+
* @param {Date} dt
|
|
53
|
+
* @returns {number} JD in UT
|
|
54
|
+
*/
|
|
55
|
+
function julianDayUT(dt) {
|
|
56
|
+
const hour =
|
|
57
|
+
dt.getUTCHours() +
|
|
58
|
+
dt.getUTCMinutes() / 60 +
|
|
59
|
+
dt.getUTCSeconds() / 3600 +
|
|
60
|
+
dt.getUTCMilliseconds() / 3600000;
|
|
61
|
+
// CalendarGregorianToJD takes a fractional day.
|
|
62
|
+
return A.julian.CalendarGregorianToJD(
|
|
63
|
+
dt.getUTCFullYear(),
|
|
64
|
+
dt.getUTCMonth() + 1,
|
|
65
|
+
dt.getUTCDate() + hour / 24
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toJDE(jdUT) {
|
|
70
|
+
const cal = A.julian.JDToCalendar(jdUT);
|
|
71
|
+
const yearFrac = cal.year + (cal.month - 1) / 12;
|
|
72
|
+
return jdUT + A.deltat.deltaT(yearFrac) / 86400;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function trueObliquity(jde) {
|
|
76
|
+
// mean obliquity + nutation in obliquity (Δε)
|
|
77
|
+
return A.nutation.meanObliquity(jde) + A.nutation.nutation(jde)[1];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sunLonDeg(jde) {
|
|
81
|
+
// apparent (of-date) ecliptic longitude: includes nutation + aberration + FK5
|
|
82
|
+
return A.solar.apparentVSOP87(EARTH, jde).lon * R2D;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function planetLonDeg(name, jde) {
|
|
86
|
+
// apparent geocentric equatorial (light-time, aberration, FK5, +Δψ, true ε)
|
|
87
|
+
const pos = A.elliptic.position(PLANETS[name], EARTH, jde);
|
|
88
|
+
const eps = trueObliquity(jde);
|
|
89
|
+
const ecl = new A.coord.Equatorial(pos.ra, pos.dec).toEcliptic(eps);
|
|
90
|
+
return ecl.lon * R2D;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function moonLonDeg(jde) {
|
|
94
|
+
// mean equinox of date, no nutation → add Δψ for apparent
|
|
95
|
+
const pos = A.moonposition.position(jde);
|
|
96
|
+
const dpsi = A.nutation.nutation(jde)[0];
|
|
97
|
+
return (pos.lon + dpsi) * R2D;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function northNodeLonDeg(jde) {
|
|
101
|
+
// TRUE lunar ascending node (oscillating), matching mainstream Human Design
|
|
102
|
+
// calculators. astronomia returns it referenced to the mean equinox of date;
|
|
103
|
+
// add Δψ for apparent.
|
|
104
|
+
const dpsi = A.nutation.nutation(jde)[0];
|
|
105
|
+
return (A.moonposition.trueNode(jde) + dpsi) * R2D;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function plutoLonDeg(jde) {
|
|
109
|
+
// pluto.astrometric returns J2000 astrometric equatorial (note: fields are
|
|
110
|
+
// _ra / _dec). Convert to J2000 ecliptic, precess J2000 → date, then add Δψ
|
|
111
|
+
// for apparent-of-date longitude.
|
|
112
|
+
//
|
|
113
|
+
// IMPORTANT: EclipticPrecessor epochs are JULIAN YEARS (e.g. 2000.0), not
|
|
114
|
+
// Julian Days — passing JD here silently produces nonsense.
|
|
115
|
+
const astro = A.pluto.astrometric(jde, EARTH);
|
|
116
|
+
const eclJ2000 = new A.coord.Equatorial(astro._ra, astro._dec).toEcliptic(
|
|
117
|
+
A.nutation.meanObliquity(2451545.0)
|
|
118
|
+
);
|
|
119
|
+
const epochTo = 2000 + (jde - 2451545.0) / 365.25;
|
|
120
|
+
const prec = new A.precess.EclipticPrecessor(2000, epochTo);
|
|
121
|
+
const moved = prec.precess(new A.coord.Ecliptic(eclJ2000.lon, eclJ2000.lat));
|
|
122
|
+
const dpsi = A.nutation.nutation(jde)[0];
|
|
123
|
+
return (moved.lon + dpsi) * R2D;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Apparent geocentric tropical ecliptic longitude of `body` at `jdUT`.
|
|
128
|
+
* @param {string} body one of BODIES
|
|
129
|
+
* @param {number} jdUT Julian Day in UT
|
|
130
|
+
* @returns {number} longitude in degrees [0, 360)
|
|
131
|
+
*/
|
|
132
|
+
function longitude(body, jdUT) {
|
|
133
|
+
const jde = toJDE(jdUT);
|
|
134
|
+
let deg;
|
|
135
|
+
switch (body) {
|
|
136
|
+
case 'sun':
|
|
137
|
+
deg = sunLonDeg(jde);
|
|
138
|
+
break;
|
|
139
|
+
case 'moon':
|
|
140
|
+
deg = moonLonDeg(jde);
|
|
141
|
+
break;
|
|
142
|
+
case 'pluto':
|
|
143
|
+
deg = plutoLonDeg(jde);
|
|
144
|
+
break;
|
|
145
|
+
case 'north_node':
|
|
146
|
+
deg = northNodeLonDeg(jde);
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
if (!PLANETS[body]) throw new Error(`astronomia backend: unknown body "${body}"`);
|
|
150
|
+
deg = planetLonDeg(body, jde);
|
|
151
|
+
}
|
|
152
|
+
return normalizeAngleDegrees(deg);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
name: 'astronomia',
|
|
157
|
+
BODIES,
|
|
158
|
+
julianDayUT,
|
|
159
|
+
longitude,
|
|
160
|
+
// exposed for reuse/debugging
|
|
161
|
+
toJDE,
|
|
162
|
+
trueObliquity,
|
|
163
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Ephemeris backend selector.
|
|
2
|
+
//
|
|
3
|
+
// rave-engine is pure-JS by default: the `astronomia` backend needs no native
|
|
4
|
+
// build and no data files. A high-precision `swisseph` backend is available
|
|
5
|
+
// opt-in (EPHE_BACKEND=swisseph) when the native binding + ephemeris files are
|
|
6
|
+
// installed — it is never required.
|
|
7
|
+
//
|
|
8
|
+
// Both backends implement the same interface:
|
|
9
|
+
// { name, BODIES, julianDayUT(dt), longitude(body, jdUT) } // degrees [0,360)
|
|
10
|
+
|
|
11
|
+
const astronomiaBackend = require('./astronomia');
|
|
12
|
+
|
|
13
|
+
let _backend = null;
|
|
14
|
+
|
|
15
|
+
function parseBool(v) {
|
|
16
|
+
if (v === undefined || v === null || v === '') return false;
|
|
17
|
+
return String(v).toLowerCase() === 'true' || String(v) === '1';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tryLoadSwisseph() {
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line global-require
|
|
23
|
+
return require('./swisseph');
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (parseBool(process.env.EPHE_DEBUG) || parseBool(process.env.EPHE_STATUS)) {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.warn(
|
|
28
|
+
`[rave-engine] EPHE_BACKEND=swisseph requested but unavailable (${e.message.split('\n')[0]}). Falling back to astronomia.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the active ephemeris backend (memoized).
|
|
37
|
+
* Default: astronomia. Opt into swisseph with EPHE_BACKEND=swisseph.
|
|
38
|
+
*/
|
|
39
|
+
function getBackend() {
|
|
40
|
+
if (_backend) return _backend;
|
|
41
|
+
if (String(process.env.EPHE_BACKEND || '').toLowerCase() === 'swisseph') {
|
|
42
|
+
const sw = tryLoadSwisseph();
|
|
43
|
+
if (sw) {
|
|
44
|
+
_backend = sw;
|
|
45
|
+
return _backend;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
_backend = astronomiaBackend;
|
|
49
|
+
return _backend;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @deprecated No-op. Kept for backward compatibility with callers that set the
|
|
54
|
+
* Swiss Ephemeris data path. The default astronomia backend needs no data files;
|
|
55
|
+
* the optional swisseph backend resolves EPHE_PATH itself.
|
|
56
|
+
*/
|
|
57
|
+
function ensureEphePath() {
|
|
58
|
+
/* intentionally empty */
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { getBackend, ensureEphePath };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Mandala / hexagram mapping helpers.
|
|
2
|
+
//
|
|
3
|
+
// Ported from preliminaries/gk_hex_profile.py to keep math identical.
|
|
4
|
+
|
|
5
|
+
const pi2 = Math.PI * 2;
|
|
6
|
+
|
|
7
|
+
// The wheel has 64 slices, each slice can be further subdivided into lines/colors/tones.
|
|
8
|
+
const hex_width = pi2 / 64;
|
|
9
|
+
const line_width = hex_width / 6;
|
|
10
|
+
const color_width = line_width / 6;
|
|
11
|
+
const tone_width = color_width / 6;
|
|
12
|
+
const base_width = tone_width / 5;
|
|
13
|
+
|
|
14
|
+
// start counting at 55, but this requires an offset in radiance!
|
|
15
|
+
const iching_map = [
|
|
16
|
+
55, 37, 63, 22, 36, 25, 17, 21, 51, 42, 3, 27, 24, 2, 23, 8,
|
|
17
|
+
20, 16, 35, 45, 12, 15, 52, 39, 53, 62, 56, 31, 33, 7, 4, 29,
|
|
18
|
+
59, 40, 64, 47, 6, 46, 18, 48, 57, 32, 50, 28, 44, 1, 43, 14,
|
|
19
|
+
34, 9, 5, 26, 11, 10, 58, 38, 54, 61, 60, 41, 19, 13, 49, 30,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const DEG_TO_RADIANS = Math.PI / 180;
|
|
23
|
+
const LINE_SEGMENT = 1 / 6;
|
|
24
|
+
|
|
25
|
+
function neutron_stream_pos(radians_position) {
|
|
26
|
+
// (2*line_width - 1*color_width - 1*tone_width) plus additional offset.
|
|
27
|
+
const offset = 2 * line_width - 1 * color_width - 1 * tone_width + 3 * base_width;
|
|
28
|
+
const offset_calc = (360 * DEG_TO_RADIANS) / 64 * 5 + offset;
|
|
29
|
+
return (((radians_position + offset_calc) / pi2) * 64) % 64;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function map_on_hexagram(fractal_line) {
|
|
33
|
+
const fractal_bin = Math.floor(fractal_line);
|
|
34
|
+
const hexagram = iching_map[fractal_bin];
|
|
35
|
+
|
|
36
|
+
const remainder = fractal_line - fractal_bin;
|
|
37
|
+
const line = Math.floor(remainder / LINE_SEGMENT) + 1;
|
|
38
|
+
|
|
39
|
+
const color_bucket = remainder - LINE_SEGMENT * Math.floor(remainder / LINE_SEGMENT);
|
|
40
|
+
const color = Math.floor(color_bucket / LINE_SEGMENT) + 1;
|
|
41
|
+
|
|
42
|
+
return { hexagram, line, color };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function map_mandala_hexagram(radians_position) {
|
|
46
|
+
const bin_value = neutron_stream_pos(radians_position);
|
|
47
|
+
return map_on_hexagram(bin_value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeAngleDegrees(angle) {
|
|
51
|
+
// [0, 360)
|
|
52
|
+
const out = angle % 360;
|
|
53
|
+
return out < 0 ? out + 360 : out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mapLongitudeDegrees(lonDeg) {
|
|
57
|
+
const lon = normalizeAngleDegrees(lonDeg);
|
|
58
|
+
return map_mandala_hexagram(lon * DEG_TO_RADIANS);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
mapLongitudeDegrees,
|
|
63
|
+
normalizeAngleDegrees,
|
|
64
|
+
// expose for debugging
|
|
65
|
+
iching_map,
|
|
66
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const { mapLongitudeDegrees, normalizeAngleDegrees } = require('./mandala');
|
|
2
|
+
const { getBackend, ensureEphePath } = require('./ephemeris');
|
|
3
|
+
|
|
4
|
+
// Longitude speed is computed by central difference (only the sign is consumed,
|
|
5
|
+
// for the `retrograde` flag, so this is extremely robust across backends).
|
|
6
|
+
const SPEED_H = 0.5; // days
|
|
7
|
+
|
|
8
|
+
function jdUtcFromDate(dt) {
|
|
9
|
+
return getBackend().julianDayUT(dt);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function signedAngleDiff(a, b) {
|
|
13
|
+
// minimal signed difference (a - b) in degrees within [-180, 180)
|
|
14
|
+
return ((a - b + 540) % 360) - 180;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function bodyLongitude(body, jdUt) {
|
|
18
|
+
return getBackend().longitude(body, jdUt);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function bodyLongitudeSpeed(body, jdUt) {
|
|
22
|
+
const hi = bodyLongitude(body, jdUt + SPEED_H);
|
|
23
|
+
const lo = bodyLongitude(body, jdUt - SPEED_H);
|
|
24
|
+
return signedAngleDiff(hi, lo) / (2 * SPEED_H);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sunLongitudeDegrees(jdUt) {
|
|
28
|
+
return bodyLongitude('sun', jdUt);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find the previous Julian Day (UT) at which the Sun's longitude was
|
|
33
|
+
* `deltaDegrees` behind its value at `jdUt`. Used to locate the Human Design
|
|
34
|
+
* "design" moment (88° of solar arc before birth).
|
|
35
|
+
*
|
|
36
|
+
* @param {number} jdUt
|
|
37
|
+
* @param {number} deltaDegrees
|
|
38
|
+
* @param {*} [_flags] deprecated/ignored (kept for backward compatibility)
|
|
39
|
+
*/
|
|
40
|
+
function findPreviousSolarLongitude(jdUt, deltaDegrees, _flags) {
|
|
41
|
+
const currentSun = sunLongitudeDegrees(jdUt);
|
|
42
|
+
const target = normalizeAngleDegrees(currentSun - deltaDegrees);
|
|
43
|
+
|
|
44
|
+
let highJd = jdUt;
|
|
45
|
+
const step = 1.0; // days
|
|
46
|
+
let lowJd = highJd - step;
|
|
47
|
+
|
|
48
|
+
let lowDiff = signedAngleDiff(sunLongitudeDegrees(lowJd), target);
|
|
49
|
+
|
|
50
|
+
let iterations = 0;
|
|
51
|
+
const maxIterations = 365;
|
|
52
|
+
|
|
53
|
+
// Walk back until we bracket the crossing.
|
|
54
|
+
while (lowDiff > 0 && iterations < maxIterations) {
|
|
55
|
+
lowJd -= step;
|
|
56
|
+
lowDiff = signedAngleDiff(sunLongitudeDegrees(lowJd), target);
|
|
57
|
+
iterations += 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (lowDiff > 0) {
|
|
61
|
+
throw new Error('Failed to bracket previous solar longitude');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Binary search for the crossing.
|
|
65
|
+
for (let i = 0; i < 50; i += 1) {
|
|
66
|
+
const mid = 0.5 * (lowJd + highJd);
|
|
67
|
+
const midDiff = signedAngleDiff(sunLongitudeDegrees(mid), target);
|
|
68
|
+
if (midDiff > 0) {
|
|
69
|
+
highJd = mid;
|
|
70
|
+
} else {
|
|
71
|
+
lowJd = mid;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return 0.5 * (lowJd + highJd);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getBody(jdUt, body) {
|
|
79
|
+
return {
|
|
80
|
+
longitude: bodyLongitude(body, jdUt),
|
|
81
|
+
longitudeSpeed: bodyLongitudeSpeed(body, jdUt),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function deriveOpposite(planet) {
|
|
86
|
+
// Earth = Sun + 180°, South Node = North Node + 180°. Speed carries over.
|
|
87
|
+
return {
|
|
88
|
+
longitude: normalizeAngleDegrees(planet.longitude + 180),
|
|
89
|
+
longitudeSpeed: planet.longitudeSpeed,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function mapPlanetToGKLine(planet) {
|
|
94
|
+
const { hexagram, line } = mapLongitudeDegrees(planet.longitude);
|
|
95
|
+
return { gk: hexagram, line };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function mapPlanetToGateLineColor(planet) {
|
|
99
|
+
const { hexagram, line, color } = mapLongitudeDegrees(planet.longitude);
|
|
100
|
+
return {
|
|
101
|
+
gate: hexagram,
|
|
102
|
+
hexagram,
|
|
103
|
+
line,
|
|
104
|
+
color,
|
|
105
|
+
longitude: planet.longitude,
|
|
106
|
+
longitude_speed: planet.longitudeSpeed,
|
|
107
|
+
retrograde: planet.longitudeSpeed < 0,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The two stream moments shared by every computation below.
|
|
112
|
+
function streamMoments({ birthUtc }) {
|
|
113
|
+
const jdPersonality = jdUtcFromDate(birthUtc);
|
|
114
|
+
const jdDesign = findPreviousSolarLongitude(jdPersonality, 88.0);
|
|
115
|
+
return { jdPersonality, jdDesign };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function computeProfileSpheres({ birthUtc }) {
|
|
119
|
+
const { jdPersonality, jdDesign } = streamMoments({ birthUtc });
|
|
120
|
+
|
|
121
|
+
// Personality bodies
|
|
122
|
+
const pSun = getBody(jdPersonality, 'sun');
|
|
123
|
+
const pMercury = getBody(jdPersonality, 'mercury');
|
|
124
|
+
const pVenus = getBody(jdPersonality, 'venus');
|
|
125
|
+
const pMars = getBody(jdPersonality, 'mars');
|
|
126
|
+
const pJupiter = getBody(jdPersonality, 'jupiter');
|
|
127
|
+
|
|
128
|
+
// Design bodies
|
|
129
|
+
const dSun = getBody(jdDesign, 'sun');
|
|
130
|
+
const dMoon = getBody(jdDesign, 'moon');
|
|
131
|
+
const dVenus = getBody(jdDesign, 'venus');
|
|
132
|
+
const dMars = getBody(jdDesign, 'mars');
|
|
133
|
+
const dJupiter = getBody(jdDesign, 'jupiter');
|
|
134
|
+
const dSaturn = getBody(jdDesign, 'saturn');
|
|
135
|
+
const dUranus = getBody(jdDesign, 'uranus');
|
|
136
|
+
|
|
137
|
+
// Earth is opposite Sun in HD.
|
|
138
|
+
const pEarth = deriveOpposite(pSun);
|
|
139
|
+
const dEarth = deriveOpposite(dSun);
|
|
140
|
+
|
|
141
|
+
// Sphere → body map (parity with event-horizon-api):
|
|
142
|
+
// - p_sun = Life's Work, p_earth = Evolution
|
|
143
|
+
// - d_sun = Radiance, d_earth = Purpose
|
|
144
|
+
// - p_venus = IQ, p_mars = EQ, p_jupiter = Pearl, p_mercury = Relating
|
|
145
|
+
// - d_moon = Attraction, d_venus = SQ, d_mars = Core, d_jupiter = Culture
|
|
146
|
+
// - d_saturn = Core Stability, d_uranus = Creativity
|
|
147
|
+
return {
|
|
148
|
+
lifeswork: mapPlanetToGKLine(pSun),
|
|
149
|
+
evolution: mapPlanetToGKLine(pEarth),
|
|
150
|
+
radiance: mapPlanetToGKLine(dSun),
|
|
151
|
+
purpose: mapPlanetToGKLine(dEarth),
|
|
152
|
+
|
|
153
|
+
iq: mapPlanetToGKLine(pVenus),
|
|
154
|
+
eq: mapPlanetToGKLine(pMars),
|
|
155
|
+
pearl: mapPlanetToGKLine(pJupiter),
|
|
156
|
+
relating: mapPlanetToGKLine(pMercury),
|
|
157
|
+
|
|
158
|
+
attraction: mapPlanetToGKLine(dMoon),
|
|
159
|
+
sq: mapPlanetToGKLine(dVenus),
|
|
160
|
+
core: mapPlanetToGKLine(dMars),
|
|
161
|
+
culture: mapPlanetToGKLine(dJupiter),
|
|
162
|
+
stability: mapPlanetToGKLine(dSaturn),
|
|
163
|
+
creativity: mapPlanetToGKLine(dUranus),
|
|
164
|
+
|
|
165
|
+
_meta: {
|
|
166
|
+
jd_personality: jdPersonality,
|
|
167
|
+
jd_design: jdDesign,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function computeEngineTest({ birthUtc }) {
|
|
173
|
+
const { jdPersonality, jdDesign } = streamMoments({ birthUtc });
|
|
174
|
+
|
|
175
|
+
// Personality bodies
|
|
176
|
+
const pSun = getBody(jdPersonality, 'sun');
|
|
177
|
+
const pMercury = getBody(jdPersonality, 'mercury');
|
|
178
|
+
const pVenus = getBody(jdPersonality, 'venus');
|
|
179
|
+
const pMars = getBody(jdPersonality, 'mars');
|
|
180
|
+
const pJupiter = getBody(jdPersonality, 'jupiter');
|
|
181
|
+
|
|
182
|
+
// Design bodies
|
|
183
|
+
const dSun = getBody(jdDesign, 'sun');
|
|
184
|
+
const dMoon = getBody(jdDesign, 'moon');
|
|
185
|
+
const dVenus = getBody(jdDesign, 'venus');
|
|
186
|
+
const dMars = getBody(jdDesign, 'mars');
|
|
187
|
+
const dJupiter = getBody(jdDesign, 'jupiter');
|
|
188
|
+
const dSaturn = getBody(jdDesign, 'saturn');
|
|
189
|
+
const dUranus = getBody(jdDesign, 'uranus');
|
|
190
|
+
|
|
191
|
+
const pEarth = deriveOpposite(pSun);
|
|
192
|
+
const dEarth = deriveOpposite(dSun);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
p_: {
|
|
196
|
+
sun: mapPlanetToGateLineColor(pSun),
|
|
197
|
+
earth: mapPlanetToGateLineColor(pEarth),
|
|
198
|
+
mercury: mapPlanetToGateLineColor(pMercury),
|
|
199
|
+
venus: mapPlanetToGateLineColor(pVenus),
|
|
200
|
+
mars: mapPlanetToGateLineColor(pMars),
|
|
201
|
+
jupiter: mapPlanetToGateLineColor(pJupiter),
|
|
202
|
+
},
|
|
203
|
+
d_: {
|
|
204
|
+
sun: mapPlanetToGateLineColor(dSun),
|
|
205
|
+
earth: mapPlanetToGateLineColor(dEarth),
|
|
206
|
+
moon: mapPlanetToGateLineColor(dMoon),
|
|
207
|
+
venus: mapPlanetToGateLineColor(dVenus),
|
|
208
|
+
mars: mapPlanetToGateLineColor(dMars),
|
|
209
|
+
jupiter: mapPlanetToGateLineColor(dJupiter),
|
|
210
|
+
saturn: mapPlanetToGateLineColor(dSaturn),
|
|
211
|
+
uranus: mapPlanetToGateLineColor(dUranus),
|
|
212
|
+
},
|
|
213
|
+
spheres: computeProfileSpheres({ birthUtc }),
|
|
214
|
+
_meta: {
|
|
215
|
+
jd_personality: jdPersonality,
|
|
216
|
+
jd_design: jdDesign,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Full Human Design activation set: all 13 bodies in BOTH streams (26 gates),
|
|
222
|
+
// used to derive the bodygraph (centers / channels). Earth and South Node are
|
|
223
|
+
// derived from Sun / North Node.
|
|
224
|
+
const HD_BODIES = [
|
|
225
|
+
'sun',
|
|
226
|
+
'earth',
|
|
227
|
+
'moon',
|
|
228
|
+
'north_node',
|
|
229
|
+
'south_node',
|
|
230
|
+
'mercury',
|
|
231
|
+
'venus',
|
|
232
|
+
'mars',
|
|
233
|
+
'jupiter',
|
|
234
|
+
'saturn',
|
|
235
|
+
'uranus',
|
|
236
|
+
'neptune',
|
|
237
|
+
'pluto',
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
function activationAt(jdUt, body) {
|
|
241
|
+
let planet;
|
|
242
|
+
if (body === 'earth') planet = deriveOpposite(getBody(jdUt, 'sun'));
|
|
243
|
+
else if (body === 'south_node') planet = deriveOpposite(getBody(jdUt, 'north_node'));
|
|
244
|
+
else planet = getBody(jdUt, body);
|
|
245
|
+
|
|
246
|
+
const { hexagram, line, color } = mapLongitudeDegrees(planet.longitude);
|
|
247
|
+
return {
|
|
248
|
+
body,
|
|
249
|
+
gate: hexagram,
|
|
250
|
+
line,
|
|
251
|
+
color,
|
|
252
|
+
longitude: planet.longitude,
|
|
253
|
+
retrograde: planet.longitudeSpeed < 0,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Compute the 26 Human Design activations (13 bodies × personality + design).
|
|
259
|
+
* @param {{ birthUtc: Date }} args
|
|
260
|
+
* @returns {{
|
|
261
|
+
* personality: Array<object>,
|
|
262
|
+
* design: Array<object>,
|
|
263
|
+
* _meta: { jd_personality: number, jd_design: number }
|
|
264
|
+
* }}
|
|
265
|
+
*/
|
|
266
|
+
function computeActivations({ birthUtc }) {
|
|
267
|
+
const { jdPersonality, jdDesign } = streamMoments({ birthUtc });
|
|
268
|
+
const personality = HD_BODIES.map((b) => ({ stream: 'personality', ...activationAt(jdPersonality, b) }));
|
|
269
|
+
const design = HD_BODIES.map((b) => ({ stream: 'design', ...activationAt(jdDesign, b) }));
|
|
270
|
+
return {
|
|
271
|
+
personality,
|
|
272
|
+
design,
|
|
273
|
+
_meta: { jd_personality: jdPersonality, jd_design: jdDesign },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
computeProfileSpheres,
|
|
279
|
+
computeEngineTest,
|
|
280
|
+
computeActivations,
|
|
281
|
+
jdUtcFromDate,
|
|
282
|
+
// Exported for reuse in global transit scanning (design stream).
|
|
283
|
+
findPreviousSolarLongitude,
|
|
284
|
+
signedAngleDiff,
|
|
285
|
+
// Kept exported for backward compatibility (now a no-op).
|
|
286
|
+
ensureEphePath,
|
|
287
|
+
HD_BODIES,
|
|
288
|
+
};
|