caelus 0.12.0 → 0.13.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 CHANGED
@@ -100,4 +100,4 @@ test/golden.test.ts conformance suite vs Python fixtures
100
100
  - caelus — this package
101
101
  - [caelus-birth](https://www.npmjs.com/package/caelus-birth) — local birth time + place → UT (charts take UT; use this)
102
102
  - [caelus-wheel](https://www.npmjs.com/package/caelus-wheel) — React SVG chart wheel
103
- - [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, nine chart tools over stdio
103
+ - [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, eighteen chart tools over stdio
package/accuracy.json CHANGED
@@ -273,7 +273,7 @@
273
273
  "counts": {
274
274
  "house_systems": 12,
275
275
  "sidereal_ayanamsas": 7,
276
- "mcp_tools": 9,
276
+ "mcp_tools": 18,
277
277
  "default_bodies": 13
278
278
  }
279
279
  }
@@ -0,0 +1,27 @@
1
+ import { Engine, BodyId } from "./chart.js";
2
+ /** Degrees of arc that equal one year of life, per time key. */
3
+ export declare const KEYS: Record<string, number>;
4
+ export declare const TRADITIONAL: BodyId[];
5
+ export interface DirectionArcs {
6
+ mc: number;
7
+ ic: number;
8
+ asc: number | null;
9
+ dsc: number | null;
10
+ }
11
+ /** Direct primary-direction arcs (degrees, [0, 360)) of a body at right
12
+ * ascension `alpha` and declination `delta` to the four angles, for latitude
13
+ * `phi` and right ascension of the MC `ramc`. `asc`/`dsc` are null when the
14
+ * body is circumpolar. */
15
+ export declare function directionArcs(alpha: number, delta: number, ramc: number, phi: number): DirectionArcs;
16
+ /** Years of life corresponding to an arc of direction under a time key. */
17
+ export declare function directionYears(arc: number, key?: string): number;
18
+ export interface PrimaryDirection {
19
+ body: string;
20
+ angle: "MC" | "IC" | "ASC" | "DSC";
21
+ arc: number;
22
+ years: number;
23
+ jd: number;
24
+ }
25
+ /** Direct primary directions of the bodies to the four angles within
26
+ * `maxYears`, by the given time key, sorted by years. */
27
+ export declare function primaryDirections(engine: Engine, natalJd: number, lat: number, lonEast: number, bodies?: BodyId[], key?: string, maxYears?: number, yearLength?: number): PrimaryDirection[];
@@ -0,0 +1,69 @@
1
+ /**
2
+ * astroengine directions -- primary directions to the angles.
3
+ *
4
+ * Primary (mundane) directions carry a planet, by the diurnal rotation of the
5
+ * sphere, to one of the four angles; the arc of rotation, converted by a time
6
+ * key, gives the age of the direction. This covers the well-defined subset:
7
+ * direct directions of a body to the MC, IC, Ascendant, and Descendant.
8
+ *
9
+ * For a body at right ascension alpha and declination delta, latitude phi, and
10
+ * right ascension of the MC (ramc): arc to MC = alpha - ramc; to IC = that
11
+ * - 180; to the Ascendant = alpha - AD - ramc - 90; to the Descendant =
12
+ * alpha + AD - ramc + 90, where AD = asin(tan phi * tan delta) is the
13
+ * ascensional difference. A circumpolar body (|tan phi * tan delta| > 1) has no
14
+ * oblique ascension, so its Ascendant/Descendant directions are undefined. Time
15
+ * keys: Ptolemy 1 deg = 1 year, Naibod 0.9856473 deg = 1 year. Mirrors the
16
+ * Python reference (astroengine/directions.py); the golden fixtures pin them.
17
+ */
18
+ import { angles } from "./houses.js";
19
+ const RAD = Math.PI / 180;
20
+ const DEG = 180 / Math.PI;
21
+ /** Degrees of arc that equal one year of life, per time key. */
22
+ export const KEYS = { ptolemy: 1.0, naibod: 0.9856473 };
23
+ export const TRADITIONAL = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn"];
24
+ const YEAR_DAYS = 365.2422;
25
+ const mod360 = (x) => ((x % 360) + 360) % 360;
26
+ /** Direct primary-direction arcs (degrees, [0, 360)) of a body at right
27
+ * ascension `alpha` and declination `delta` to the four angles, for latitude
28
+ * `phi` and right ascension of the MC `ramc`. `asc`/`dsc` are null when the
29
+ * body is circumpolar. */
30
+ export function directionArcs(alpha, delta, ramc, phi) {
31
+ const arcMc = mod360(alpha - ramc);
32
+ const arcIc = mod360(alpha - ramc - 180);
33
+ const t = Math.tan(phi * RAD) * Math.tan(delta * RAD);
34
+ if (Math.abs(t) > 1)
35
+ return { mc: arcMc, ic: arcIc, asc: null, dsc: null };
36
+ const ad = Math.asin(t) * DEG;
37
+ return {
38
+ mc: arcMc, ic: arcIc,
39
+ asc: mod360(alpha - ad - ramc - 90),
40
+ dsc: mod360(alpha + ad - ramc + 90),
41
+ };
42
+ }
43
+ /** Years of life corresponding to an arc of direction under a time key. */
44
+ export function directionYears(arc, key = "naibod") {
45
+ return arc / KEYS[key];
46
+ }
47
+ /** Direct primary directions of the bodies to the four angles within
48
+ * `maxYears`, by the given time key, sorted by years. */
49
+ export function primaryDirections(engine, natalJd, lat, lonEast, bodies = TRADITIONAL, key = "naibod", maxYears = 90, yearLength = YEAR_DAYS) {
50
+ const armc = angles(engine.data, natalJd, lat, lonEast)[2];
51
+ const ramc = armc * DEG;
52
+ const out = [];
53
+ const ANGLES = ["mc", "ic", "asc", "dsc"];
54
+ for (const b of bodies) {
55
+ const p = engine.position(b, natalJd);
56
+ const arcs = directionArcs(p.ra, p.dec, ramc, lat);
57
+ for (const angle of ANGLES) {
58
+ const arc = arcs[angle];
59
+ if (arc === null)
60
+ continue;
61
+ const years = directionYears(arc, key);
62
+ if (years <= maxYears) {
63
+ out.push({ body: b, angle: angle.toUpperCase(), arc, years, jd: natalJd + years * yearLength });
64
+ }
65
+ }
66
+ }
67
+ out.sort((a, b) => a.years - b.years);
68
+ return out;
69
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * astroengine firdaria -- the Persian/medieval system of planetary time-lord
3
+ * periods (firdariyyat).
4
+ *
5
+ * Life divides into nine periods totalling 75 years: the seven planets in the
6
+ * firdaria order, then the two lunar nodes. A day chart begins with the Sun, a
7
+ * night chart with the Moon; both follow the same cycle (Sun, Venus, Mercury,
8
+ * Moon, Saturn, Jupiter, Mars) and close with the North and South Nodes. Each
9
+ * planetary period splits into seven equal sub-periods led by the seven planets
10
+ * from that period's lord; node periods have no sub-divisions. Pure time
11
+ * arithmetic on the natal moment and the chart's sect. Mirrors the Python
12
+ * reference (astroengine/firdaria.py); the golden fixtures pin the two together.
13
+ */
14
+ import { Engine } from "./chart.js";
15
+ /** The firdaria cycle of the seven planets. */
16
+ export declare const FIRDARIA_ORDER: readonly ["sun", "venus", "mercury", "moon", "saturn", "jupiter", "mars"];
17
+ /** Period length in years for each of the seven planets. */
18
+ export declare const FIRDARIA_YEARS: Record<(typeof FIRDARIA_ORDER)[number], number>;
19
+ /** The two nodes close the sequence with no sub-periods (70 + 5 = 75 years). */
20
+ export declare const NODE_PERIODS: ReadonlyArray<readonly [string, number]>;
21
+ /** The nine major firdaria periods in order, as `[lord, years]` pairs. */
22
+ export declare function firdariaSequence(day: boolean): Array<[string, number]>;
23
+ export interface FirdariaSub {
24
+ lord: string;
25
+ start: number;
26
+ end: number;
27
+ }
28
+ export interface FirdariaPeriod {
29
+ lord: string;
30
+ years: number;
31
+ start: number;
32
+ end: number;
33
+ sub: FirdariaSub[];
34
+ }
35
+ /** The full firdaria timeline from birth. */
36
+ export declare function firdaria(day: boolean, natalJd: number, yearLength?: number): FirdariaPeriod[];
37
+ /** The major and sub firdar lord active at `targetJd`; both null outside the
38
+ * 75-year span. */
39
+ export declare function firdariaActive(day: boolean, natalJd: number, targetJd: number, yearLength?: number): {
40
+ major: string | null;
41
+ sub: string | null;
42
+ };
43
+ /** The active firdar at `targetJd`, taking the chart's sect from the natal
44
+ * moment and place. */
45
+ export declare function firdariaAt(engine: Engine, natalJd: number, targetJd: number, lat: number, lonEast: number, yearLength?: number): {
46
+ day: boolean;
47
+ major: string | null;
48
+ sub: string | null;
49
+ };
@@ -0,0 +1,62 @@
1
+ import { TROPICAL_YEAR, isDayChart } from "./derived.js";
2
+ /** The firdaria cycle of the seven planets. */
3
+ export const FIRDARIA_ORDER = [
4
+ "sun", "venus", "mercury", "moon", "saturn", "jupiter", "mars",
5
+ ];
6
+ /** Period length in years for each of the seven planets. */
7
+ export const FIRDARIA_YEARS = {
8
+ sun: 10, venus: 8, mercury: 13, moon: 9, saturn: 11, jupiter: 12, mars: 7,
9
+ };
10
+ /** The two nodes close the sequence with no sub-periods (70 + 5 = 75 years). */
11
+ export const NODE_PERIODS = [
12
+ ["north_node", 3], ["south_node", 2],
13
+ ];
14
+ /** The nine major firdaria periods in order, as `[lord, years]` pairs. */
15
+ export function firdariaSequence(day) {
16
+ const start = day ? 0 : FIRDARIA_ORDER.indexOf("moon");
17
+ const planets = [];
18
+ for (let i = 0; i < 7; i++) {
19
+ const lord = FIRDARIA_ORDER[(start + i) % 7];
20
+ planets.push([lord, FIRDARIA_YEARS[lord]]);
21
+ }
22
+ return [...planets, ...NODE_PERIODS.map((p) => [p[0], p[1]])];
23
+ }
24
+ /** The full firdaria timeline from birth. */
25
+ export function firdaria(day, natalJd, yearLength = TROPICAL_YEAR) {
26
+ const out = [];
27
+ let t = natalJd;
28
+ for (const [lord, years] of firdariaSequence(day)) {
29
+ const span = years * yearLength;
30
+ const major = { lord, years, start: t, end: t + span, sub: [] };
31
+ const li = FIRDARIA_ORDER.indexOf(lord);
32
+ if (li >= 0) {
33
+ const subSpan = span / 7;
34
+ let st = t;
35
+ for (let k = 0; k < 7; k++) {
36
+ const sl = FIRDARIA_ORDER[(li + k) % 7];
37
+ major.sub.push({ lord: sl, start: st, end: st + subSpan });
38
+ st += subSpan;
39
+ }
40
+ }
41
+ out.push(major);
42
+ t += span;
43
+ }
44
+ return out;
45
+ }
46
+ /** The major and sub firdar lord active at `targetJd`; both null outside the
47
+ * 75-year span. */
48
+ export function firdariaActive(day, natalJd, targetJd, yearLength = TROPICAL_YEAR) {
49
+ for (const major of firdaria(day, natalJd, yearLength)) {
50
+ if (major.start <= targetJd && targetJd < major.end) {
51
+ const sub = major.sub.find((s) => s.start <= targetJd && targetJd < s.end);
52
+ return { major: major.lord, sub: sub ? sub.lord : null };
53
+ }
54
+ }
55
+ return { major: null, sub: null };
56
+ }
57
+ /** The active firdar at `targetJd`, taking the chart's sect from the natal
58
+ * moment and place. */
59
+ export function firdariaAt(engine, natalJd, targetJd, lat, lonEast, yearLength = TROPICAL_YEAR) {
60
+ const day = isDayChart(engine, natalJd, lat, lonEast);
61
+ return { day, ...firdariaActive(day, natalJd, targetJd, yearLength) };
62
+ }
@@ -15,3 +15,12 @@ export * from "./astrocartography.js";
15
15
  export * from "./ephemeris.js";
16
16
  export * from "./features.js";
17
17
  export * from "./compiler.js";
18
+ export * from "./lots.js";
19
+ export * from "./profections.js";
20
+ export * from "./firdaria.js";
21
+ export * from "./releasing.js";
22
+ export * from "./vedic.js";
23
+ export * from "./directions.js";
24
+ export * from "./vargas.js";
25
+ export * from "./yogini.js";
26
+ export * from "./yogas.js";
package/dist/src/index.js CHANGED
@@ -15,3 +15,12 @@ export * from "./astrocartography.js";
15
15
  export * from "./ephemeris.js";
16
16
  export * from "./features.js";
17
17
  export * from "./compiler.js";
18
+ export * from "./lots.js";
19
+ export * from "./profections.js";
20
+ export * from "./firdaria.js";
21
+ export * from "./releasing.js";
22
+ export * from "./vedic.js";
23
+ export * from "./directions.js";
24
+ export * from "./vargas.js";
25
+ export * from "./yogini.js";
26
+ export * from "./yogas.js";
@@ -0,0 +1,18 @@
1
+ import { Engine, Zodiac } from "./chart.js";
2
+ /** The seven Hermetic lots, in their conventional order. */
3
+ export declare const HERMETIC_LOTS: readonly ["fortune", "spirit", "eros", "necessity", "courage", "victory", "nemesis"];
4
+ export type HermeticLot = (typeof HERMETIC_LOTS)[number];
5
+ /** Lot of Fortune: Asc + Moon - Sun by day, Asc + Sun - Moon by night. */
6
+ export declare function lotFortune(asc: number, sun: number, moon: number, day: boolean): number;
7
+ /** Lot of Spirit (the reverse of Fortune): Asc + Sun - Moon by day. */
8
+ export declare function lotSpirit(asc: number, sun: number, moon: number, day: boolean): number;
9
+ /** The seven Hermetic lots from the Ascendant, sect, and the seven planets'
10
+ * longitudes (degrees). Pure arithmetic. */
11
+ export declare function hermeticLots(asc: number, day: boolean, sun: number, moon: number, mercury: number, venus: number, mars: number, jupiter: number, saturn: number): Record<HermeticLot, number>;
12
+ export interface ChartLots extends Record<HermeticLot, number> {
13
+ /** True when the Sun is above the horizon (a diurnal chart). */
14
+ day: boolean;
15
+ }
16
+ /** The seven Hermetic lots of a chart: compute the Ascendant and sect, then the
17
+ * lots from the seven planets' longitudes. */
18
+ export declare function lots(engine: Engine, jdUt: number, lat: number, lonEast: number, zodiac?: Zodiac): ChartLots;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * astroengine lots -- Hellenistic lots (Arabic parts), sect-aware.
3
+ *
4
+ * A lot is an arc cast from the Ascendant equal to the arc between two chart
5
+ * points, reversing direction between a day and a night chart. Arithmetic on
6
+ * apparent longitudes already checked against Swiss Ephemeris. Mirrors the
7
+ * Python reference (astroengine/lots.py); the golden fixtures pin the two
8
+ * together. Fortune and Spirit are symmetric about the Ascendant, so
9
+ * `(fortune + spirit) === 2 * asc` (mod 360).
10
+ */
11
+ import { mod } from "./core.js";
12
+ import { isDayChart } from "./derived.js";
13
+ /** The seven Hermetic lots, in their conventional order. */
14
+ export const HERMETIC_LOTS = [
15
+ "fortune", "spirit", "eros", "necessity", "courage", "victory", "nemesis",
16
+ ];
17
+ /** Asc + (a - b) by day, Asc + (b - a) by night, wrapped to [0, 360). */
18
+ function lot(asc, a, b, day) {
19
+ return mod(asc + (day ? a - b : b - a), 360);
20
+ }
21
+ /** Lot of Fortune: Asc + Moon - Sun by day, Asc + Sun - Moon by night. */
22
+ export function lotFortune(asc, sun, moon, day) {
23
+ return lot(asc, moon, sun, day);
24
+ }
25
+ /** Lot of Spirit (the reverse of Fortune): Asc + Sun - Moon by day. */
26
+ export function lotSpirit(asc, sun, moon, day) {
27
+ return lot(asc, sun, moon, day);
28
+ }
29
+ /** The seven Hermetic lots from the Ascendant, sect, and the seven planets'
30
+ * longitudes (degrees). Pure arithmetic. */
31
+ export function hermeticLots(asc, day, sun, moon, mercury, venus, mars, jupiter, saturn) {
32
+ const fortune = lotFortune(asc, sun, moon, day);
33
+ const spirit = lotSpirit(asc, sun, moon, day);
34
+ return {
35
+ fortune,
36
+ spirit,
37
+ eros: lot(asc, venus, spirit, day),
38
+ necessity: lot(asc, fortune, mercury, day),
39
+ courage: lot(asc, fortune, mars, day),
40
+ victory: lot(asc, jupiter, spirit, day),
41
+ nemesis: lot(asc, fortune, saturn, day),
42
+ };
43
+ }
44
+ /** The seven Hermetic lots of a chart: compute the Ascendant and sect, then the
45
+ * lots from the seven planets' longitudes. */
46
+ export function lots(engine, jdUt, lat, lonEast, zodiac = "tropical") {
47
+ const asc = engine.chartAt(jdUt, lat, lonEast, { zodiac }).angles.asc;
48
+ const day = isDayChart(engine, jdUt, lat, lonEast);
49
+ const lon = (b) => engine.longitude(b, jdUt, { zodiac });
50
+ const h = hermeticLots(asc, day, lon("sun"), lon("moon"), lon("mercury"), lon("venus"), lon("mars"), lon("jupiter"), lon("saturn"));
51
+ return { day, ...h };
52
+ }
@@ -0,0 +1,27 @@
1
+ import { Engine, Zodiac } from "./chart.js";
2
+ /** Traditional (domicile) ruler of each sign, Aries..Pisces. */
3
+ export declare const SIGN_RULERS: readonly ["mars", "venus", "mercury", "moon", "sun", "mercury", "venus", "mars", "jupiter", "saturn", "saturn", "jupiter"];
4
+ /** Traditional (domicile) ruler of a sign index (0 = Aries). */
5
+ export declare function signRuler(sign: number): string;
6
+ export interface ProfectedSign {
7
+ sign: string;
8
+ sign_index: number;
9
+ /** 1-based whole-sign house from the natal Ascendant. */
10
+ house: number;
11
+ /** Traditional (domicile) lord of the profected sign. */
12
+ lord: string;
13
+ }
14
+ /** The whole-sign profection `steps` signs after the Ascendant sign. */
15
+ export declare function profectedSign(ascSign: number, steps: number): ProfectedSign;
16
+ export interface Profection {
17
+ age_years: number;
18
+ /** 1-based month within the profection year. */
19
+ month: number;
20
+ annual: ProfectedSign;
21
+ monthly: ProfectedSign;
22
+ }
23
+ /** Annual and monthly profection at `targetJd` for a natal Ascendant sign. */
24
+ export declare function profection(ascSign: number, natalJd: number, targetJd: number, yearLength?: number): Profection;
25
+ /** Profection from a natal chart: take the Ascendant sign from the natal chart,
26
+ * then profect to `targetJd`. */
27
+ export declare function profectionAt(engine: Engine, natalJd: number, targetJd: number, lat: number, lonEast: number, zodiac?: Zodiac, yearLength?: number): Profection;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * astroengine profections -- annual and monthly profections, a Hellenistic
3
+ * time-lord technique.
4
+ *
5
+ * The Ascendant advances one whole sign per year of life; the profected sign's
6
+ * traditional (domicile) ruler is the lord of the year. Within the year the
7
+ * monthly profection advances one further sign per 1/12 of the year. Pure
8
+ * arithmetic on a date difference and the natal Ascendant sign. Mirrors the
9
+ * Python reference (astroengine/profections.py); the golden fixtures pin the
10
+ * two together. Whole-sign frame: the sign N signs after the Ascendant is the
11
+ * (N+1)th house. The profection year is a fixed length (the tropical year by
12
+ * default); birthday-exact profections would key off the solar return.
13
+ */
14
+ import { mod } from "./core.js";
15
+ import { SIGNS } from "./chart.js";
16
+ import { TROPICAL_YEAR } from "./derived.js";
17
+ /** Traditional (domicile) ruler of each sign, Aries..Pisces. */
18
+ export const SIGN_RULERS = [
19
+ "mars", "venus", "mercury", "moon", "sun", "mercury",
20
+ "venus", "mars", "jupiter", "saturn", "saturn", "jupiter",
21
+ ];
22
+ /** Traditional (domicile) ruler of a sign index (0 = Aries). */
23
+ export function signRuler(sign) {
24
+ return SIGN_RULERS[mod(sign, 12)];
25
+ }
26
+ /** The whole-sign profection `steps` signs after the Ascendant sign. */
27
+ export function profectedSign(ascSign, steps) {
28
+ const sign = mod(ascSign + steps, 12);
29
+ return { sign: SIGNS[sign], sign_index: sign, house: mod(steps, 12) + 1, lord: signRuler(sign) };
30
+ }
31
+ /** Annual and monthly profection at `targetJd` for a natal Ascendant sign. */
32
+ export function profection(ascSign, natalJd, targetJd, yearLength = TROPICAL_YEAR) {
33
+ const age = (targetJd - natalJd) / yearLength;
34
+ const years = Math.floor(age);
35
+ const month = Math.floor((age - years) * 12); // 0..11
36
+ return {
37
+ age_years: years,
38
+ month: month + 1,
39
+ annual: profectedSign(ascSign, years),
40
+ monthly: profectedSign(ascSign, years + month),
41
+ };
42
+ }
43
+ /** Profection from a natal chart: take the Ascendant sign from the natal chart,
44
+ * then profect to `targetJd`. */
45
+ export function profectionAt(engine, natalJd, targetJd, lat, lonEast, zodiac = "tropical", yearLength = TROPICAL_YEAR) {
46
+ const asc = engine.chartAt(natalJd, lat, lonEast, { zodiac }).angles.asc;
47
+ const ascSign = mod(Math.floor(asc / 30), 12);
48
+ return profection(ascSign, natalJd, targetJd, yearLength);
49
+ }
@@ -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, 3, 9, 10, 12];
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,52 @@
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, 3, 9, 10, 12];
21
+ function vargaSign(rasi, div, n) {
22
+ switch (n) {
23
+ case 1: return rasi;
24
+ case 3: return (rasi + 4 * div) % 12;
25
+ case 9: return (NAVAMSA_START[rasi % 4] + div) % 12;
26
+ case 10: return rasi % 2 === 0 ? (rasi + div) % 12 : (rasi + 8 + div) % 12;
27
+ case 12: return (rasi + div) % 12;
28
+ default: throw new Error(`unsupported varga D${n}`);
29
+ }
30
+ }
31
+ /** The varga D-n placement of a sidereal longitude. */
32
+ export function varga(siderealLon, n) {
33
+ const lon = ((siderealLon % 360) + 360) % 360;
34
+ const rasi = Math.floor(lon / 30) % 12;
35
+ const within = lon - rasi * 30;
36
+ let div = Math.floor(within / (30 / n));
37
+ if (div >= n)
38
+ div = n - 1; // guard a boundary rounding to n
39
+ const s = vargaSign(rasi, div, n);
40
+ return { varga: n, rasi: SIGNS[rasi], rasi_index: rasi, sign: SIGNS[s], sign_index: s, division: div + 1 };
41
+ }
42
+ /** The varga D-n of a body (default the Moon) at jd, in a sidereal zodiac. */
43
+ export function vargaAt(engine, jdUt, n, body = "moon", zodiac = "sidereal:lahiri") {
44
+ return varga(engine.longitude(body, jdUt, { zodiac }), n);
45
+ }
46
+ /** The full divisional chart D-n at jd: the varga sign of each body. */
47
+ export function vargaChart(engine, jdUt, n, bodies = BODIES, zodiac = "sidereal:lahiri") {
48
+ const out = {};
49
+ for (const b of bodies)
50
+ out[b] = varga(engine.longitude(b, jdUt, { zodiac }), n);
51
+ return out;
52
+ }
@@ -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,26 @@
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
+ /** The placement yogas of a natal chart, from the sidereal rasi positions. */
26
+ export declare function yogasAt(engine: Engine, natalJd: number, lat: number, lonEast: number, zodiac?: Zodiac): Yoga[];
@@ -0,0 +1,39 @@
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
+ /** The placement yogas of a natal chart, from the sidereal rasi positions. */
32
+ export function yogasAt(engine, natalJd, lat, lonEast, zodiac = "sidereal:lahiri") {
33
+ const chart = engine.chartAt(natalJd, lat, lonEast, { zodiac });
34
+ const ascSign = Math.floor(chart.angles.asc / 30) % 12;
35
+ const signs = {};
36
+ for (const b of YOGA_PLANETS)
37
+ signs[b] = Math.floor(chart.bodies[b].lon / 30) % 12;
38
+ return detectYogas(signs, ascSign);
39
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * astroengine yogini -- the Yogini dasha, a 36-year nakshatra-based dasha cycle.
3
+ *
4
+ * Eight yoginis rule in a fixed order with periods 1..8 years (totalling 36):
5
+ * Mangala (Moon) 1, Pingala (Sun) 2, Dhanya (Jupiter) 3, Bhramari (Mars) 4,
6
+ * Bhadrika (Mercury) 5, Ulka (Saturn) 6, Siddha (Venus) 7, Sankata (Rahu) 8.
7
+ * The starting yogini comes from the Moon's birth nakshatra: (nakshatra number
8
+ * + 3) mod 8, a remainder of 0 meaning the 8th. As in Vimshottari, the elapsed
9
+ * portion of the first period is the fraction of the nakshatra the Moon has
10
+ * traversed, and each period subdivides into eight proportional sub-periods.
11
+ * Mirrors the Python reference (astroengine/yogini.py); the golden fixtures pin
12
+ * the two together.
13
+ */
14
+ import { Engine, Zodiac } from "./chart.js";
15
+ export declare const YOGINIS: readonly ["Mangala", "Pingala", "Dhanya", "Bhramari", "Bhadrika", "Ulka", "Siddha", "Sankata"];
16
+ export declare const YOGINI_LORDS: Record<(typeof YOGINIS)[number], string>;
17
+ /** Period in years by yogini index (Mangala..Sankata), totalling 36. */
18
+ export declare const YOGINI_YEARS: readonly [1, 2, 3, 4, 5, 6, 7, 8];
19
+ /** 0-based starting yogini index from the Moon's nakshatra index (0-based):
20
+ * (nakshatra number + 3) mod 8, a remainder of 0 mapping to the 8th. */
21
+ export declare function startingYogini(nakIndex: number): number;
22
+ export interface YoginiSub {
23
+ yogini: string;
24
+ lord: string;
25
+ start: number;
26
+ end: number;
27
+ }
28
+ export interface YoginiPeriod {
29
+ level: number;
30
+ yogini: string;
31
+ lord: string;
32
+ years: number;
33
+ start: number;
34
+ end: number;
35
+ sub: YoginiSub[];
36
+ }
37
+ export interface YoginiTimeline {
38
+ start_yogini: string;
39
+ balance_years: number;
40
+ dashas: YoginiPeriod[];
41
+ }
42
+ /** The Yogini dasha timeline from the Moon's sidereal longitude. */
43
+ export declare function yoginiDashas(moonLon: number, natalJd: number, levels?: number, yearLength?: number, count?: number): YoginiTimeline;
44
+ export interface YoginiActive {
45
+ maha: string;
46
+ antar: string | null;
47
+ }
48
+ /** The maha and antar yogini active at targetJd; null before the first period. */
49
+ export declare function yoginiActive(moonLon: number, natalJd: number, targetJd: number, yearLength?: number): YoginiActive | null;
50
+ /** Yogini dasha active at targetJd, from the natal Moon's nakshatra. */
51
+ export declare function yoginiAt(engine: Engine, natalJd: number, targetJd: number, zodiac?: Zodiac, yearLength?: number): {
52
+ moon_nakshatra: string;
53
+ start_yogini: string;
54
+ } & Partial<YoginiActive>;
@@ -0,0 +1,63 @@
1
+ import { nakshatra, NAK_SPAN, DASHA_YEAR } from "./vedic.js";
2
+ export const YOGINIS = [
3
+ "Mangala", "Pingala", "Dhanya", "Bhramari", "Bhadrika", "Ulka", "Siddha", "Sankata",
4
+ ];
5
+ export const YOGINI_LORDS = {
6
+ Mangala: "moon", Pingala: "sun", Dhanya: "jupiter", Bhramari: "mars",
7
+ Bhadrika: "mercury", Ulka: "saturn", Siddha: "venus", Sankata: "rahu",
8
+ };
9
+ /** Period in years by yogini index (Mangala..Sankata), totalling 36. */
10
+ export const YOGINI_YEARS = [1, 2, 3, 4, 5, 6, 7, 8];
11
+ const YOGINI_TOTAL = 36;
12
+ /** 0-based starting yogini index from the Moon's nakshatra index (0-based):
13
+ * (nakshatra number + 3) mod 8, a remainder of 0 mapping to the 8th. */
14
+ export function startingYogini(nakIndex) {
15
+ const y = (nakIndex + 1 + 3) % 8; // nakshatra number is 1-based
16
+ return ((y - 1) % 8 + 8) % 8; // remainder 0 -> 8th (index 7)
17
+ }
18
+ /** The Yogini dasha timeline from the Moon's sidereal longitude. */
19
+ export function yoginiDashas(moonLon, natalJd, levels = 2, yearLength = DASHA_YEAR, count = 8) {
20
+ const nak = nakshatra(moonLon);
21
+ const start = startingYogini(nak.index);
22
+ const elapsed = nak.pos / NAK_SPAN;
23
+ const y0 = YOGINI_YEARS[start];
24
+ let t = natalJd - elapsed * y0 * yearLength;
25
+ const dashas = [];
26
+ for (let k = 0; k < count; k++) {
27
+ const yi = (start + k) % 8;
28
+ const years = YOGINI_YEARS[yi];
29
+ const span = years * yearLength;
30
+ const maha = {
31
+ level: 1, yogini: YOGINIS[yi], lord: YOGINI_LORDS[YOGINIS[yi]],
32
+ years, start: t, end: t + span, sub: [],
33
+ };
34
+ if (levels >= 2) {
35
+ let st = t;
36
+ for (let j = 0; j < 8; j++) {
37
+ const sj = (yi + j) % 8;
38
+ const subSpan = (years * YOGINI_YEARS[sj] / YOGINI_TOTAL) * yearLength;
39
+ maha.sub.push({ yogini: YOGINIS[sj], lord: YOGINI_LORDS[YOGINIS[sj]], start: st, end: st + subSpan });
40
+ st += subSpan;
41
+ }
42
+ }
43
+ dashas.push(maha);
44
+ t += span;
45
+ }
46
+ return { start_yogini: YOGINIS[start], balance_years: (1 - elapsed) * y0, dashas };
47
+ }
48
+ /** The maha and antar yogini active at targetJd; null before the first period. */
49
+ export function yoginiActive(moonLon, natalJd, targetJd, yearLength = DASHA_YEAR) {
50
+ const timeline = yoginiDashas(moonLon, natalJd, 2, yearLength, 24).dashas;
51
+ const maha = timeline.find((p) => p.start <= targetJd && targetJd < p.end);
52
+ if (!maha)
53
+ return null;
54
+ const antar = maha.sub.find((s) => s.start <= targetJd && targetJd < s.end);
55
+ return { maha: maha.yogini, antar: antar ? antar.yogini : null };
56
+ }
57
+ /** Yogini dasha active at targetJd, from the natal Moon's nakshatra. */
58
+ export function yoginiAt(engine, natalJd, targetJd, zodiac = "sidereal:lahiri", yearLength = DASHA_YEAR) {
59
+ const moonLon = engine.longitude("moon", natalJd, { zodiac });
60
+ const nak = nakshatra(moonLon);
61
+ const active = yoginiActive(moonLon, natalJd, targetJd, yearLength) ?? {};
62
+ return { moon_nakshatra: nak.name, start_yogini: YOGINIS[startingYogini(nak.index)], ...active };
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caelus",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Astrological ephemeris engine. MIT, no AGPL, no ephemeris files. Checked against Swiss Ephemeris.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",