caelus 0.12.0 → 0.14.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.
@@ -0,0 +1,105 @@
1
+ import { SIGN_RULERS } from "./profections.js";
2
+ /** Graha drishti: the house-distances (1-based) each planet aspects. */
3
+ export const DRISHTI = {
4
+ sun: [7], moon: [7], mercury: [7], venus: [7],
5
+ mars: [4, 7, 8], jupiter: [5, 7, 9], saturn: [3, 7, 10],
6
+ };
7
+ export const KENDRAS = [1, 4, 7, 10];
8
+ export const TRIKONAS = [1, 5, 9];
9
+ export const DHANA_HOUSES = [2, 5, 9, 11];
10
+ const PURE_KENDRAS = [4, 7, 10];
11
+ const PURE_TRIKONAS = [5, 9];
12
+ const PLANETS = ["sun", "moon", "mars", "mercury", "jupiter", "venus", "saturn"];
13
+ /** The traditional ruler of a sign index (0 = Aries). */
14
+ export function signLord(sign) {
15
+ return SIGN_RULERS[((sign % 12) + 12) % 12];
16
+ }
17
+ /** The sign on the given whole-sign house (1-12) from the Ascendant. */
18
+ export function houseSign(ascSign, house) {
19
+ return (ascSign + house - 1) % 12;
20
+ }
21
+ /** The lord of the given whole-sign house from the Ascendant. */
22
+ export function houseLord(ascSign, house) {
23
+ return signLord(houseSign(ascSign, house));
24
+ }
25
+ /** The whole-sign house (1-12) a sign falls in from the Ascendant. */
26
+ export function houseFromAsc(ascSign, sign) {
27
+ return ((sign - ascSign) % 12 + 12) % 12 + 1;
28
+ }
29
+ /** Whether `planet` at `planetSign` casts a graha drishti onto `targetSign`. */
30
+ export function aspectsSign(planet, planetSign, targetSign) {
31
+ const dist = ((targetSign - planetSign) % 12 + 12) % 12 + 1;
32
+ return (DRISHTI[planet] ?? [7]).includes(dist);
33
+ }
34
+ /** Sign exchange: a sits in b's sign and b sits in a's sign. */
35
+ export function parivartana(planetA, signA, planetB, signB) {
36
+ return signLord(signA) === planetB && signLord(signB) === planetA;
37
+ }
38
+ /** How two planets associate: "conjunction" | "exchange" | "aspect" | null. */
39
+ export function associationType(planetA, signA, planetB, signB) {
40
+ if (planetA === planetB)
41
+ return null;
42
+ if (signA === signB)
43
+ return "conjunction";
44
+ if (parivartana(planetA, signA, planetB, signB))
45
+ return "exchange";
46
+ if (aspectsSign(planetA, signA, signB) && aspectsSign(planetB, signB, signA))
47
+ return "aspect";
48
+ return null;
49
+ }
50
+ /** Planets ruling both a pure kendra (4/7/10) and a pure trikona (5/9), sorted. */
51
+ export function yogakarakas(ascSign) {
52
+ const out = [];
53
+ for (const p of PLANETS) {
54
+ const ruled = new Set();
55
+ for (let h = 1; h <= 12; h++)
56
+ if (houseLord(ascSign, h) === p)
57
+ ruled.add(h);
58
+ if (PURE_KENDRAS.some((h) => ruled.has(h)) && PURE_TRIKONAS.some((h) => ruled.has(h)))
59
+ out.push(p);
60
+ }
61
+ return out.sort();
62
+ }
63
+ function lordPairYogas(ascSign, signs, housesA, housesB) {
64
+ const lordsA = [...new Set(housesA.map((h) => houseLord(ascSign, h)))].sort();
65
+ const lordsB = [...new Set(housesB.map((h) => houseLord(ascSign, h)))].sort();
66
+ const seen = new Map();
67
+ for (const la of lordsA) {
68
+ for (const lb of lordsB) {
69
+ const via = associationType(la, signs[la], lb, signs[lb]);
70
+ if (via === null)
71
+ continue;
72
+ const pair = [la, lb].sort().join("|");
73
+ if (!seen.has(pair))
74
+ seen.set(pair, via);
75
+ }
76
+ }
77
+ return [...seen.entries()].sort((a, b) => a[0].localeCompare(b[0]))
78
+ .map(([pair, via]) => ({ lords: pair.split("|"), via }));
79
+ }
80
+ /** Raja yogas: associations between a kendra lord and a trikona lord. */
81
+ export function rajaYogas(signs, ascSign) {
82
+ return lordPairYogas(ascSign, signs, KENDRAS, TRIKONAS);
83
+ }
84
+ /** Dhana yogas: associations between two wealth-house (2/5/9/11) lords. */
85
+ export function dhanaYogas(signs, ascSign) {
86
+ return lordPairYogas(ascSign, signs, DHANA_HOUSES, DHANA_HOUSES);
87
+ }
88
+ function signsOf(engine, natalJd, lat, lonEast, zodiac) {
89
+ const chart = engine.chartAt(natalJd, lat, lonEast, { zodiac });
90
+ const ascSign = Math.floor(chart.angles.asc / 30) % 12;
91
+ const signs = {};
92
+ for (const p of PLANETS)
93
+ signs[p] = Math.floor(chart.bodies[p].lon / 30) % 12;
94
+ return { signs, ascSign };
95
+ }
96
+ /** Raja yogas of a natal chart, with the chart's yogakarakas. */
97
+ export function rajaYogasAt(engine, natalJd, lat, lonEast, zodiac = "sidereal:lahiri") {
98
+ const { signs, ascSign } = signsOf(engine, natalJd, lat, lonEast, zodiac);
99
+ return { raja: rajaYogas(signs, ascSign), yogakarakas: yogakarakas(ascSign) };
100
+ }
101
+ /** Dhana yogas of a natal chart. */
102
+ export function dhanaYogasAt(engine, natalJd, lat, lonEast, zodiac = "sidereal:lahiri") {
103
+ const { signs, ascSign } = signsOf(engine, natalJd, lat, lonEast, zodiac);
104
+ return dhanaYogas(signs, ascSign);
105
+ }
@@ -0,0 +1,32 @@
1
+ import { Engine, Zodiac } from "./chart.js";
2
+ /** Period length in 360-day years for each sign, Aries..Pisces. */
3
+ export declare const ZR_PERIODS: readonly [15, 8, 20, 25, 19, 20, 8, 15, 12, 27, 30, 12];
4
+ /** Days per period unit at each level (each a twelfth of the one above). */
5
+ export declare const LEVEL_UNIT: Record<number, number>;
6
+ export interface ZrPeriod {
7
+ level: number;
8
+ sign: string;
9
+ lord: string;
10
+ start: number;
11
+ end: number;
12
+ /** True when this period is the loosing of the bond (jumped to the opposite sign). */
13
+ lb: boolean;
14
+ }
15
+ /** Flat timeline of releasing periods down to `maxLevel` over `horizonYears`
16
+ * (360-day years) from birth. */
17
+ export declare function zrRelease(lotSign: number, natalJd: number, maxLevel?: number, horizonYears?: number): ZrPeriod[];
18
+ export interface ZrActive {
19
+ l1: string;
20
+ l2: string;
21
+ l3: string;
22
+ l4: string;
23
+ }
24
+ /** The L1..L4 releasing signs active at `targetJd`; null outside the span. */
25
+ export declare function zrActive(lotSign: number, natalJd: number, targetJd: number): ZrActive | null;
26
+ /** Zodiacal releasing active at `targetJd`, releasing from the Lot of Spirit
27
+ * (default) or Fortune of the natal chart. */
28
+ export declare function zrAt(engine: Engine, natalJd: number, targetJd: number, lat: number, lonEast: number, lot?: "spirit" | "fortune", zodiac?: Zodiac): {
29
+ lot: string;
30
+ lot_sign: string;
31
+ day: boolean;
32
+ } & Partial<ZrActive>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * astroengine releasing -- zodiacal releasing (aphesis), the Hellenistic
3
+ * time-lord technique from Vettius Valens, released from a Lot (usually Spirit
4
+ * or Fortune).
5
+ *
6
+ * From the Lot's sign, periods release sign by sign. A sign's period length is
7
+ * its planetary minor years (Aries 15, Taurus 8, Gemini 20, Cancer 25, Leo 19,
8
+ * Virgo 20, Libra 8, Scorpio 15, Sagittarius 12, Capricorn 27, Aquarius 30,
9
+ * Pisces 12). Convention: 360-day years, each level a twelfth of the one above
10
+ * (L1 = period x 360 days, L2 = x 30, L3 = x 2.5, L4 = x 2.5/12). Within a
11
+ * period the next level releases from the same sign and fills it (last
12
+ * sub-period truncates at the boundary); when a sub-level returns to the sign it
13
+ * began on it looses the bond, jumping to the opposite sign (+6) once. Mirrors
14
+ * the Python reference (astroengine/releasing.py); the golden fixtures pin the
15
+ * two together.
16
+ */
17
+ import { SIGNS } from "./chart.js";
18
+ import { isDayChart } from "./derived.js";
19
+ import { lotSpirit, lotFortune } from "./lots.js";
20
+ import { SIGN_RULERS } from "./profections.js";
21
+ /** Period length in 360-day years for each sign, Aries..Pisces. */
22
+ export const ZR_PERIODS = [15, 8, 20, 25, 19, 20, 8, 15, 12, 27, 30, 12];
23
+ /** Days per period unit at each level (each a twelfth of the one above). */
24
+ export const LEVEL_UNIT = { 1: 360, 2: 30, 3: 2.5, 4: 2.5 / 12 };
25
+ const FULL_CYCLE = ZR_PERIODS.reduce((a, b) => a + b, 0) * 360; // one full L1 cycle, days
26
+ const EPS = 1e-9;
27
+ function release(out, level, maxLevel, startSign, spanStart, spanEnd, horizon) {
28
+ const unit = LEVEL_UNIT[level];
29
+ let sign = startSign;
30
+ let lb = false;
31
+ let pendingLb = false;
32
+ let cur = spanStart;
33
+ while (cur < spanEnd - EPS && cur < horizon - EPS) {
34
+ const plen = ZR_PERIODS[sign] * unit;
35
+ const subEnd = Math.min(cur + plen, spanEnd, horizon);
36
+ out.push({ level, sign: SIGNS[sign], lord: SIGN_RULERS[sign], start: cur, end: subEnd, lb: pendingLb });
37
+ if (level < maxLevel) {
38
+ release(out, level + 1, maxLevel, sign, cur, Math.min(cur + plen, spanEnd), horizon);
39
+ }
40
+ cur += plen;
41
+ pendingLb = false;
42
+ const nxt = (sign + 1) % 12;
43
+ if (nxt === startSign && !lb) {
44
+ sign = (startSign + 6) % 12;
45
+ lb = true;
46
+ pendingLb = true;
47
+ }
48
+ else {
49
+ sign = nxt;
50
+ }
51
+ }
52
+ }
53
+ /** Flat timeline of releasing periods down to `maxLevel` over `horizonYears`
54
+ * (360-day years) from birth. */
55
+ export function zrRelease(lotSign, natalJd, maxLevel = 2, horizonYears = 100) {
56
+ const out = [];
57
+ const horizon = natalJd + horizonYears * 360;
58
+ release(out, 1, maxLevel, lotSign, natalJd, natalJd + FULL_CYCLE, horizon);
59
+ return out;
60
+ }
61
+ /** The (sign, start, end) of the sub-period containing `target`, or null. */
62
+ function subAt(unit, startSign, spanStart, spanEnd, target) {
63
+ let sign = startSign;
64
+ let lb = false;
65
+ let cur = spanStart;
66
+ while (cur < spanEnd - EPS) {
67
+ const plen = ZR_PERIODS[sign] * unit;
68
+ const subEnd = Math.min(cur + plen, spanEnd);
69
+ if (cur <= target && target < subEnd)
70
+ return [sign, cur, subEnd];
71
+ cur += plen;
72
+ const nxt = (sign + 1) % 12;
73
+ if (nxt === startSign && !lb) {
74
+ sign = (startSign + 6) % 12;
75
+ lb = true;
76
+ }
77
+ else
78
+ sign = nxt;
79
+ }
80
+ return null;
81
+ }
82
+ /** The L1..L4 releasing signs active at `targetJd`; null outside the span. */
83
+ export function zrActive(lotSign, natalJd, targetJd) {
84
+ const l1 = subAt(360, lotSign, natalJd, natalJd + FULL_CYCLE, targetJd);
85
+ if (l1 === null)
86
+ return null;
87
+ const l2 = subAt(30, l1[0], l1[1], l1[2], targetJd);
88
+ if (l2 === null)
89
+ return null;
90
+ const l3 = subAt(2.5, l2[0], l2[1], l2[2], targetJd);
91
+ if (l3 === null)
92
+ return null;
93
+ const l4 = subAt(2.5 / 12, l3[0], l3[1], l3[2], targetJd);
94
+ if (l4 === null)
95
+ return null;
96
+ return { l1: SIGNS[l1[0]], l2: SIGNS[l2[0]], l3: SIGNS[l3[0]], l4: SIGNS[l4[0]] };
97
+ }
98
+ /** Zodiacal releasing active at `targetJd`, releasing from the Lot of Spirit
99
+ * (default) or Fortune of the natal chart. */
100
+ export function zrAt(engine, natalJd, targetJd, lat, lonEast, lot = "spirit", zodiac = "tropical") {
101
+ const asc = engine.chartAt(natalJd, lat, lonEast, { zodiac }).angles.asc;
102
+ const day = isDayChart(engine, natalJd, lat, lonEast);
103
+ const sun = engine.longitude("sun", natalJd, { zodiac });
104
+ const moon = engine.longitude("moon", natalJd, { zodiac });
105
+ const lotLon = (lot === "spirit" ? lotSpirit : lotFortune)(asc, sun, moon, day);
106
+ const lotSign = ((Math.floor(lotLon / 30) % 12) + 12) % 12;
107
+ const active = zrActive(lotSign, natalJd, targetJd) ?? {};
108
+ return { lot, lot_sign: SIGNS[lotSign], day, ...active };
109
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * astroengine vargas -- Vedic divisional charts (vargas).
3
+ *
4
+ * A varga D-n divides each 30-degree sign into n equal parts and maps each part
5
+ * to a sign by a classical (Parashari) rule. This covers the unambiguous,
6
+ * textbook set: D1 (rasi), D3 (drekkana), D9 (navamsa), D10 (dasamsa), and D12
7
+ * (dwadasamsa); the contested hora (D2) and unequal trimsamsa (D30) are left to
8
+ * a later step. Rules (rasi/div 0-based; an "odd sign" is the 1st, 3rd, ... =
9
+ * even rasi index): D1 the sign; D3 (rasi + 4*div); D9 element start
10
+ * ([Aries, Capricorn, Libra, Cancer] by element) + div; D10 odd (rasi + div),
11
+ * even (rasi + 8 + div); D12 (rasi + div). Computed from rasi = floor(lon/30)
12
+ * and div = floor(within/(30/n)) so sign boundaries stay robust and
13
+ * `Math.floor` matches Python's `math.floor`. Built on the validated sidereal
14
+ * longitudes. Mirrors the Python reference (astroengine/vargas.py).
15
+ */
16
+ import { Engine, BodyId, Zodiac } from "./chart.js";
17
+ /** Supported divisions. */
18
+ export declare const VARGA_DIVISIONS: readonly [1, 2, 3, 9, 10, 12, 30];
19
+ export interface Varga {
20
+ varga: number;
21
+ rasi: string;
22
+ rasi_index: number;
23
+ sign: string;
24
+ sign_index: number;
25
+ division: number;
26
+ }
27
+ /** The varga D-n placement of a sidereal longitude. */
28
+ export declare function varga(siderealLon: number, n: number): Varga;
29
+ /** The varga D-n of a body (default the Moon) at jd, in a sidereal zodiac. */
30
+ export declare function vargaAt(engine: Engine, jdUt: number, n: number, body?: BodyId, zodiac?: Zodiac): Varga;
31
+ /** The full divisional chart D-n at jd: the varga sign of each body. */
32
+ export declare function vargaChart(engine: Engine, jdUt: number, n: number, bodies?: BodyId[], zodiac?: Zodiac): Record<string, Varga>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * astroengine vargas -- Vedic divisional charts (vargas).
3
+ *
4
+ * A varga D-n divides each 30-degree sign into n equal parts and maps each part
5
+ * to a sign by a classical (Parashari) rule. This covers the unambiguous,
6
+ * textbook set: D1 (rasi), D3 (drekkana), D9 (navamsa), D10 (dasamsa), and D12
7
+ * (dwadasamsa); the contested hora (D2) and unequal trimsamsa (D30) are left to
8
+ * a later step. Rules (rasi/div 0-based; an "odd sign" is the 1st, 3rd, ... =
9
+ * even rasi index): D1 the sign; D3 (rasi + 4*div); D9 element start
10
+ * ([Aries, Capricorn, Libra, Cancer] by element) + div; D10 odd (rasi + div),
11
+ * even (rasi + 8 + div); D12 (rasi + div). Computed from rasi = floor(lon/30)
12
+ * and div = floor(within/(30/n)) so sign boundaries stay robust and
13
+ * `Math.floor` matches Python's `math.floor`. Built on the validated sidereal
14
+ * longitudes. Mirrors the Python reference (astroengine/vargas.py).
15
+ */
16
+ import { BODIES, SIGNS } from "./chart.js";
17
+ /** Element start sign for the navamsa (fire, earth, air, water by rasi % 4). */
18
+ const NAVAMSA_START = [0, 9, 6, 3];
19
+ /** Supported divisions. */
20
+ export const VARGA_DIVISIONS = [1, 2, 3, 9, 10, 12, 30];
21
+ // Trimsamsa (D30): five unequal degree-bands per sign mapping to a non-luminary's
22
+ // sign. Odd: Mars 0-5 -> Aries, Saturn 5-10 -> Aquarius, Jupiter 10-18 ->
23
+ // Sagittarius, Mercury 18-25 -> Gemini, Venus 25-30 -> Libra. Even reverses with
24
+ // the planets' even signs. Each is [upper-degree-bound, result sign index].
25
+ const TRIMSAMSA_ODD = [[5, 0], [10, 10], [18, 8], [25, 2], [30, 6]];
26
+ const TRIMSAMSA_EVEN = [[5, 1], [12, 5], [20, 11], [25, 9], [30, 7]];
27
+ function trimsamsa(rasi, within) {
28
+ const bands = rasi % 2 === 0 ? TRIMSAMSA_ODD : TRIMSAMSA_EVEN;
29
+ for (let i = 0; i < bands.length; i++)
30
+ if (within < bands[i][0])
31
+ return [bands[i][1], i + 1];
32
+ return [bands[bands.length - 1][1], 5];
33
+ }
34
+ function vargaSign(rasi, div, n) {
35
+ switch (n) {
36
+ case 1: return rasi;
37
+ // Parashari hora: odd sign first half -> Leo, second half -> Cancer; even
38
+ // sign reversed (odd sign == even rasi index).
39
+ case 2: return ((rasi % 2 === 0) === (div === 0)) ? 4 : 3;
40
+ case 3: return (rasi + 4 * div) % 12;
41
+ case 9: return (NAVAMSA_START[rasi % 4] + div) % 12;
42
+ case 10: return rasi % 2 === 0 ? (rasi + div) % 12 : (rasi + 8 + div) % 12;
43
+ case 12: return (rasi + div) % 12;
44
+ default: throw new Error(`unsupported varga D${n}`);
45
+ }
46
+ }
47
+ /** The varga D-n placement of a sidereal longitude. */
48
+ export function varga(siderealLon, n) {
49
+ const lon = ((siderealLon % 360) + 360) % 360;
50
+ const rasi = Math.floor(lon / 30) % 12;
51
+ const within = lon - rasi * 30;
52
+ let s;
53
+ let division;
54
+ if (n === 30) { // trimsamsa: unequal bands
55
+ [s, division] = trimsamsa(rasi, within);
56
+ }
57
+ else {
58
+ let div = Math.floor(within / (30 / n));
59
+ if (div >= n)
60
+ div = n - 1; // guard a boundary rounding to n
61
+ s = vargaSign(rasi, div, n);
62
+ division = div + 1;
63
+ }
64
+ return { varga: n, rasi: SIGNS[rasi], rasi_index: rasi, sign: SIGNS[s], sign_index: s, division };
65
+ }
66
+ /** The varga D-n of a body (default the Moon) at jd, in a sidereal zodiac. */
67
+ export function vargaAt(engine, jdUt, n, body = "moon", zodiac = "sidereal:lahiri") {
68
+ return varga(engine.longitude(body, jdUt, { zodiac }), n);
69
+ }
70
+ /** The full divisional chart D-n at jd: the varga sign of each body. */
71
+ export function vargaChart(engine, jdUt, n, bodies = BODIES, zodiac = "sidereal:lahiri") {
72
+ const out = {};
73
+ for (const b of bodies)
74
+ out[b] = varga(engine.longitude(b, jdUt, { zodiac }), n);
75
+ return out;
76
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * astroengine vedic -- the Vedic/Jyotish layer: nakshatras and the Vimshottari
3
+ * dasha, built on the already-validated sidereal longitudes.
4
+ *
5
+ * A nakshatra is one of 27 equal lunar mansions of 13 deg 20' in the sidereal
6
+ * zodiac, each with four padas and a ruling planet cycling Ketu, Venus, Sun,
7
+ * Moon, Mars, Rahu, Jupiter, Saturn, Mercury. The Vimshottari dasha is a
8
+ * 120-year sequence of planetary periods in that order; the starting dasha is
9
+ * the lord of the Moon's birth nakshatra, with the elapsed portion set by how
10
+ * far the Moon has moved through it. Mahadashas subdivide into antardashas and
11
+ * pratyantardashas of the nine lords, proportional to their years. Nakshatra
12
+ * placement is exact division of the sidereal longitude; the dasha year is a
13
+ * fixed 365.25 days by default (the common Jyotish convention). Mirrors the
14
+ * Python reference (astroengine/vedic.py); the golden fixtures pin the two.
15
+ */
16
+ import { Engine, BodyId, Zodiac } from "./chart.js";
17
+ export declare const NAKSHATRAS: readonly ["Ashwini", "Bharani", "Krittika", "Rohini", "Mrigashira", "Ardra", "Punarvasu", "Pushya", "Ashlesha", "Magha", "Purva Phalguni", "Uttara Phalguni", "Hasta", "Chitra", "Swati", "Vishakha", "Anuradha", "Jyeshtha", "Mula", "Purva Ashadha", "Uttara Ashadha", "Shravana", "Dhanishta", "Shatabhisha", "Purva Bhadrapada", "Uttara Bhadrapada", "Revati"];
18
+ /** The Vimshottari order and each lord's period in years (totalling 120). */
19
+ export declare const VIMSHOTTARI_ORDER: readonly ["ketu", "venus", "sun", "moon", "mars", "rahu", "jupiter", "saturn", "mercury"];
20
+ export declare const VIMSHOTTARI_YEARS: Record<(typeof VIMSHOTTARI_ORDER)[number], number>;
21
+ export declare const NAK_SPAN: number;
22
+ export declare const DASHA_YEAR = 365.25;
23
+ export interface Nakshatra {
24
+ index: number;
25
+ name: string;
26
+ pada: number;
27
+ lord: string;
28
+ /** Degrees into the nakshatra, 0..13.333. */
29
+ pos: number;
30
+ }
31
+ /** The nakshatra of a sidereal longitude. */
32
+ export declare function nakshatra(siderealLon: number): Nakshatra;
33
+ /** The nakshatra of a body (default the Moon) at jd, in a sidereal zodiac. */
34
+ export declare function nakshatraAt(engine: Engine, jdUt: number, body?: BodyId, zodiac?: Zodiac): Nakshatra;
35
+ export interface DashaSub {
36
+ lord: string;
37
+ start: number;
38
+ end: number;
39
+ }
40
+ export interface Dasha {
41
+ level: number;
42
+ lord: string;
43
+ start: number;
44
+ end: number;
45
+ sub: DashaSub[];
46
+ }
47
+ export interface DashaTimeline {
48
+ start_lord: string;
49
+ balance_years: number;
50
+ dashas: Dasha[];
51
+ }
52
+ /** The Vimshottari dasha timeline from the Moon's sidereal longitude. */
53
+ export declare function vimshottariDashas(moonLon: number, natalJd: number, levels?: number, yearLength?: number, count?: number): DashaTimeline;
54
+ export interface DashaActive {
55
+ maha: string;
56
+ antar: string | null;
57
+ pratyantar: string | null;
58
+ }
59
+ /** The mahadasha, antardasha, and pratyantardasha lords active at targetJd. */
60
+ export declare function vimshottariActive(moonLon: number, natalJd: number, targetJd: number, yearLength?: number): DashaActive | null;
61
+ /** Vimshottari dasha active at targetJd, from the natal Moon's nakshatra. */
62
+ export declare function vimshottariAt(engine: Engine, natalJd: number, targetJd: number, zodiac?: Zodiac, yearLength?: number): {
63
+ moon_nakshatra: string;
64
+ moon_pada: number;
65
+ start_lord: string;
66
+ } & Partial<DashaActive>;
@@ -0,0 +1,101 @@
1
+ export const NAKSHATRAS = [
2
+ "Ashwini", "Bharani", "Krittika", "Rohini", "Mrigashira", "Ardra",
3
+ "Punarvasu", "Pushya", "Ashlesha", "Magha", "Purva Phalguni",
4
+ "Uttara Phalguni", "Hasta", "Chitra", "Swati", "Vishakha", "Anuradha",
5
+ "Jyeshtha", "Mula", "Purva Ashadha", "Uttara Ashadha", "Shravana",
6
+ "Dhanishta", "Shatabhisha", "Purva Bhadrapada", "Uttara Bhadrapada", "Revati",
7
+ ];
8
+ /** The Vimshottari order and each lord's period in years (totalling 120). */
9
+ export const VIMSHOTTARI_ORDER = [
10
+ "ketu", "venus", "sun", "moon", "mars", "rahu", "jupiter", "saturn", "mercury",
11
+ ];
12
+ export const VIMSHOTTARI_YEARS = {
13
+ ketu: 7, venus: 20, sun: 6, moon: 10, mars: 7, rahu: 18, jupiter: 16, saturn: 19, mercury: 17,
14
+ };
15
+ export const NAK_SPAN = 360 / 27; // 13 deg 20'
16
+ const VIMSHOTTARI_TOTAL = 120;
17
+ export const DASHA_YEAR = 365.25; // days per dasha-year (common Jyotish convention)
18
+ function mod360(x) {
19
+ return ((x % 360) + 360) % 360;
20
+ }
21
+ /** The nakshatra of a sidereal longitude. */
22
+ export function nakshatra(siderealLon) {
23
+ const lon = mod360(siderealLon);
24
+ const i = Math.floor(lon / NAK_SPAN) % 27;
25
+ const pos = lon - i * NAK_SPAN;
26
+ const pada = Math.floor(pos / (NAK_SPAN / 4)) + 1;
27
+ return { index: i, name: NAKSHATRAS[i], pada, lord: VIMSHOTTARI_ORDER[i % 9], pos };
28
+ }
29
+ /** The nakshatra of a body (default the Moon) at jd, in a sidereal zodiac. */
30
+ export function nakshatraAt(engine, jdUt, body = "moon", zodiac = "sidereal:lahiri") {
31
+ return nakshatra(engine.longitude(body, jdUt, { zodiac }));
32
+ }
33
+ /** The Vimshottari dasha timeline from the Moon's sidereal longitude. */
34
+ export function vimshottariDashas(moonLon, natalJd, levels = 2, yearLength = DASHA_YEAR, count = 9) {
35
+ const lon = mod360(moonLon);
36
+ const nakI = Math.floor(lon / NAK_SPAN) % 27;
37
+ const pos = lon - nakI * NAK_SPAN;
38
+ const startLord = VIMSHOTTARI_ORDER[nakI % 9];
39
+ const elapsed = pos / NAK_SPAN;
40
+ const y0 = VIMSHOTTARI_YEARS[startLord];
41
+ const li = VIMSHOTTARI_ORDER.indexOf(startLord);
42
+ let t = natalJd - elapsed * y0 * yearLength;
43
+ const dashas = [];
44
+ for (let k = 0; k < count; k++) {
45
+ const lord = VIMSHOTTARI_ORDER[(li + k) % 9];
46
+ const years = VIMSHOTTARI_YEARS[lord];
47
+ const span = years * yearLength;
48
+ const maha = { level: 1, lord, start: t, end: t + span, sub: [] };
49
+ if (levels >= 2) {
50
+ const sli = VIMSHOTTARI_ORDER.indexOf(lord);
51
+ let st = t;
52
+ for (let j = 0; j < 9; j++) {
53
+ const sl = VIMSHOTTARI_ORDER[(sli + j) % 9];
54
+ const subSpan = (years * VIMSHOTTARI_YEARS[sl] / VIMSHOTTARI_TOTAL) * yearLength;
55
+ maha.sub.push({ lord: sl, start: st, end: st + subSpan });
56
+ st += subSpan;
57
+ }
58
+ }
59
+ dashas.push(maha);
60
+ t += span;
61
+ }
62
+ return { start_lord: startLord, balance_years: (1 - elapsed) * y0, dashas };
63
+ }
64
+ function activeIn(periods, target) {
65
+ return periods.find((p) => p.start <= target && target < p.end) ?? null;
66
+ }
67
+ /** The mahadasha, antardasha, and pratyantardasha lords active at targetJd. */
68
+ export function vimshottariActive(moonLon, natalJd, targetJd, yearLength = DASHA_YEAR) {
69
+ const timeline = vimshottariDashas(moonLon, natalJd, 2, yearLength, 10).dashas;
70
+ const maha = activeIn(timeline, targetJd);
71
+ if (maha === null)
72
+ return null;
73
+ const antar = activeIn(maha.sub, targetJd);
74
+ if (antar === null)
75
+ return { maha: maha.lord, antar: null, pratyantar: null };
76
+ const ay = VIMSHOTTARI_YEARS[maha.lord]
77
+ * VIMSHOTTARI_YEARS[antar.lord] / VIMSHOTTARI_TOTAL;
78
+ const sli = VIMSHOTTARI_ORDER.indexOf(antar.lord);
79
+ let st = antar.start;
80
+ let pratyantar = null;
81
+ for (let j = 0; j < 9; j++) {
82
+ const sl = VIMSHOTTARI_ORDER[(sli + j) % 9];
83
+ const span = (ay * VIMSHOTTARI_YEARS[sl] / VIMSHOTTARI_TOTAL) * yearLength;
84
+ if (st <= targetJd && targetJd < st + span) {
85
+ pratyantar = sl;
86
+ break;
87
+ }
88
+ st += span;
89
+ }
90
+ return { maha: maha.lord, antar: antar.lord, pratyantar };
91
+ }
92
+ /** Vimshottari dasha active at targetJd, from the natal Moon's nakshatra. */
93
+ export function vimshottariAt(engine, natalJd, targetJd, zodiac = "sidereal:lahiri", yearLength = DASHA_YEAR) {
94
+ const moonLon = engine.longitude("moon", natalJd, { zodiac });
95
+ const nak = nakshatra(moonLon);
96
+ const active = vimshottariActive(moonLon, natalJd, targetJd, yearLength) ?? {};
97
+ return {
98
+ moon_nakshatra: nak.name, moon_pada: nak.pada,
99
+ start_lord: VIMSHOTTARI_ORDER[nak.index % 9], ...active,
100
+ };
101
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * astroengine yogas -- classical Vedic yogas (planetary combinations) judged on
3
+ * the sidereal rasi (D1) chart.
4
+ *
5
+ * Covers the well-defined, placement-based yogas with no textual variation: the
6
+ * five Pancha Mahapurusha yogas (a non-luminary in its own sign or exaltation
7
+ * AND in a kendra from the Ascendant -- Ruchaka/Mars, Bhadra/Mercury,
8
+ * Hamsa/Jupiter, Malavya/Venus, Shasha/Saturn); Gajakesari (Jupiter in a kendra
9
+ * from the Moon); Budha-Aditya (Sun and Mercury in one sign); and
10
+ * Chandra-Mangala (Moon and Mars in one sign). Own-sign/exaltation use the
11
+ * engine's `dignities`; houses are whole-sign from the Ascendant. The
12
+ * variant-laden yogas (Kemadruma, lordship-based raja/dhana) are left to a later
13
+ * step. Mirrors the Python reference (astroengine/yogas.py).
14
+ */
15
+ import { Engine, BodyId, Zodiac } from "./chart.js";
16
+ export declare const YOGA_PLANETS: BodyId[];
17
+ export interface Yoga {
18
+ yoga: string;
19
+ planets: string[];
20
+ }
21
+ /** The placement yogas present in a chart. `signs` maps each of the seven
22
+ * classical planets to its 0-based sign index; `ascSign` is the Ascendant's
23
+ * sign index. */
24
+ export declare function detectYogas(signs: Record<string, number>, ascSign: number): Yoga[];
25
+ export interface Kemadruma {
26
+ present: boolean;
27
+ planets_checked: string[];
28
+ }
29
+ /** Kemadruma yoga: the Moon is isolated -- no planet in the 2nd or 12th sign
30
+ * from it, nor conjunct it. The planet set is parameterized (texts vary): the
31
+ * default is the five tara grahas, `includeSun` adds the Sun, `includeNodes`
32
+ * adds Rahu/Ketu when present in `signs`. */
33
+ export declare function kemadruma(signs: Record<string, number>, includeSun?: boolean, includeNodes?: boolean): Kemadruma;
34
+ /** Kemadruma yoga of a natal chart, from the sidereal rasi positions. */
35
+ export declare function kemadrumaAt(engine: Engine, natalJd: number, lat: number, lonEast: number, includeSun?: boolean, includeNodes?: boolean, zodiac?: Zodiac): Kemadruma;
36
+ /** The placement yogas of a natal chart, from the sidereal rasi positions. */
37
+ export declare function yogasAt(engine: Engine, natalJd: number, lat: number, lonEast: number, zodiac?: Zodiac): Yoga[];
@@ -0,0 +1,68 @@
1
+ import { dignities } from "./derived.js";
2
+ /** Pancha Mahapurusha: [yoga name, planet]. */
3
+ const MAHAPURUSHA = [
4
+ ["Ruchaka", "mars"], ["Bhadra", "mercury"], ["Hamsa", "jupiter"],
5
+ ["Malavya", "venus"], ["Shasha", "saturn"],
6
+ ];
7
+ const KENDRA = new Set([1, 4, 7, 10]);
8
+ export const YOGA_PLANETS = ["sun", "moon", "mars", "mercury", "jupiter", "venus", "saturn"];
9
+ /** The placement yogas present in a chart. `signs` maps each of the seven
10
+ * classical planets to its 0-based sign index; `ascSign` is the Ascendant's
11
+ * sign index. */
12
+ export function detectYogas(signs, ascSign) {
13
+ const house = (sign) => ((sign - ascSign) % 12 + 12) % 12 + 1;
14
+ const out = [];
15
+ for (const [name, p] of MAHAPURUSHA) {
16
+ const dig = dignities(p, signs[p]);
17
+ if ((dig.includes("domicile") || dig.includes("exaltation")) && KENDRA.has(house(signs[p]))) {
18
+ out.push({ yoga: name, planets: [p] });
19
+ }
20
+ }
21
+ const jkFromMoon = ((signs.jupiter - signs.moon) % 12 + 12) % 12;
22
+ if (jkFromMoon === 0 || jkFromMoon === 3 || jkFromMoon === 6 || jkFromMoon === 9) {
23
+ out.push({ yoga: "Gajakesari", planets: ["jupiter", "moon"] });
24
+ }
25
+ if (signs.sun === signs.mercury)
26
+ out.push({ yoga: "Budha-Aditya", planets: ["sun", "mercury"] });
27
+ if (signs.moon === signs.mars)
28
+ out.push({ yoga: "Chandra-Mangala", planets: ["moon", "mars"] });
29
+ return out;
30
+ }
31
+ /** Kemadruma yoga: the Moon is isolated -- no planet in the 2nd or 12th sign
32
+ * from it, nor conjunct it. The planet set is parameterized (texts vary): the
33
+ * default is the five tara grahas, `includeSun` adds the Sun, `includeNodes`
34
+ * adds Rahu/Ketu when present in `signs`. */
35
+ export function kemadruma(signs, includeSun = false, includeNodes = false) {
36
+ let planets = ["mars", "mercury", "jupiter", "venus", "saturn"];
37
+ if (includeSun)
38
+ planets = ["sun", ...planets];
39
+ if (includeNodes)
40
+ planets = [...planets, "rahu", "ketu"];
41
+ planets = planets.filter((p) => p in signs);
42
+ const moon = signs.moon;
43
+ const occupied = new Set([((moon - 1) % 12 + 12) % 12, moon, (moon + 1) % 12]);
44
+ const present = !planets.some((p) => occupied.has(signs[p]));
45
+ return { present, planets_checked: planets };
46
+ }
47
+ /** Kemadruma yoga of a natal chart, from the sidereal rasi positions. */
48
+ export function kemadrumaAt(engine, natalJd, lat, lonEast, includeSun = false, includeNodes = false, zodiac = "sidereal:lahiri") {
49
+ const chart = engine.chartAt(natalJd, lat, lonEast, { zodiac });
50
+ const bodies = includeNodes ? [...YOGA_PLANETS, "mean_node"] : YOGA_PLANETS;
51
+ const signs = {};
52
+ for (const b of bodies)
53
+ signs[b] = Math.floor(chart.bodies[b].lon / 30) % 12;
54
+ if (includeNodes) {
55
+ signs.rahu = signs.mean_node;
56
+ signs.ketu = (signs.mean_node + 6) % 12;
57
+ }
58
+ return kemadruma(signs, includeSun, includeNodes);
59
+ }
60
+ /** The placement yogas of a natal chart, from the sidereal rasi positions. */
61
+ export function yogasAt(engine, natalJd, lat, lonEast, zodiac = "sidereal:lahiri") {
62
+ const chart = engine.chartAt(natalJd, lat, lonEast, { zodiac });
63
+ const ascSign = Math.floor(chart.angles.asc / 30) % 12;
64
+ const signs = {};
65
+ for (const b of YOGA_PLANETS)
66
+ signs[b] = Math.floor(chart.bodies[b].lon / 30) % 12;
67
+ return detectYogas(signs, ascSign);
68
+ }