caelus 0.13.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.
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, eighteen chart tools over stdio
103
+ - [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, twenty-two 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": 18,
276
+ "mcp_tools": 22,
277
277
  "default_bodies": 13
278
278
  }
279
279
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * astroengine ashtottari -- the Ashtottari dasha, a 108-year conditional dasha.
3
+ *
4
+ * Eight lords rule in order -- Sun 6, Moon 15, Mars 8, Mercury 17, Saturn 10,
5
+ * Jupiter 19, Rahu 12, Venus 21 years (totalling 108). Unlike Vimshottari the
6
+ * nakshatra-to-lord mapping is in irregular groups, and the elapsed portion of
7
+ * the first period is measured across the lord's whole multi-nakshatra span.
8
+ *
9
+ * Convention: the JHora/PVR Narasimha Rao mapping (the PyJHora implementation),
10
+ * which the texts vary around; the lord ranges and across-span balance are
11
+ * reproduced from it, validated against the named source in `validate_jyotish`
12
+ * rather than asserted. Mirrors the Python reference (astroengine/ashtottari.py);
13
+ * the golden fixtures pin the two together.
14
+ */
15
+ import { Engine, Zodiac } from "./chart.js";
16
+ export declare const ASHTOTTARI_ORDER: readonly ["sun", "moon", "mars", "mercury", "saturn", "jupiter", "rahu", "venus"];
17
+ export declare const ASHTOTTARI_YEARS: Record<(typeof ASHTOTTARI_ORDER)[number], number>;
18
+ /** The Ashtottari dasha lord governing a nakshatra index (0-based). */
19
+ export declare function ashtottariLord(nakIndex: number): string;
20
+ export interface AshtottariSub {
21
+ lord: string;
22
+ start: number;
23
+ end: number;
24
+ }
25
+ export interface AshtottariPeriod {
26
+ level: number;
27
+ lord: string;
28
+ years: number;
29
+ start: number;
30
+ end: number;
31
+ sub: AshtottariSub[];
32
+ }
33
+ export interface AshtottariTimeline {
34
+ start_lord: string;
35
+ balance_years: number;
36
+ dashas: AshtottariPeriod[];
37
+ }
38
+ /** The Ashtottari dasha timeline from the Moon's sidereal longitude. */
39
+ export declare function ashtottariDashas(moonLon: number, natalJd: number, levels?: number, yearLength?: number, count?: number): AshtottariTimeline;
40
+ export interface AshtottariActive {
41
+ maha: string;
42
+ antar: string | null;
43
+ }
44
+ /** The maha and antar lord active at targetJd; null before the first period. */
45
+ export declare function ashtottariActive(moonLon: number, natalJd: number, targetJd: number, yearLength?: number): AshtottariActive | null;
46
+ /** Ashtottari dasha active at targetJd, from the natal Moon's nakshatra. */
47
+ export declare function ashtottariAt(engine: Engine, natalJd: number, targetJd: number, zodiac?: Zodiac, yearLength?: number): {
48
+ moon_nakshatra: string;
49
+ start_lord: string;
50
+ } & Partial<AshtottariActive>;
@@ -0,0 +1,71 @@
1
+ import { nakshatra, NAK_SPAN, DASHA_YEAR } from "./vedic.js";
2
+ export const ASHTOTTARI_ORDER = [
3
+ "sun", "moon", "mars", "mercury", "saturn", "jupiter", "rahu", "venus",
4
+ ];
5
+ export const ASHTOTTARI_YEARS = {
6
+ sun: 6, moon: 15, mars: 8, mercury: 17, saturn: 10, jupiter: 19, rahu: 12, venus: 21,
7
+ };
8
+ const ASHTOTTARI_TOTAL = 108;
9
+ // Each lord's nakshatra group: [lord, start nakshatra index, span]. Rahu wraps
10
+ // past 27 (nakshatras 26, 0, 1, 2).
11
+ const ASHTOTTARI_RANGES = [
12
+ ["sun", 6, 4], ["moon", 10, 3], ["mars", 13, 4], ["mercury", 17, 3],
13
+ ["saturn", 20, 3], ["jupiter", 23, 3], ["rahu", 26, 4], ["venus", 3, 3],
14
+ ];
15
+ /** The Ashtottari dasha lord governing a nakshatra index (0-based). */
16
+ export function ashtottariLord(nakIndex) {
17
+ for (const [lord, start, span] of ASHTOTTARI_RANGES) {
18
+ if (((nakIndex - start) % 27 + 27) % 27 < span)
19
+ return lord;
20
+ }
21
+ throw new Error(`no Ashtottari lord for nakshatra ${nakIndex}`);
22
+ }
23
+ /** The Ashtottari dasha timeline from the Moon's sidereal longitude. */
24
+ export function ashtottariDashas(moonLon, natalJd, levels = 2, yearLength = DASHA_YEAR, count = 8) {
25
+ const lon = ((moonLon % 360) + 360) % 360;
26
+ const nakI = Math.floor(lon / NAK_SPAN) % 27;
27
+ const startLord = ashtottariLord(nakI);
28
+ const [, startNak, spanNak] = ASHTOTTARI_RANGES.find((r) => r[0] === startLord);
29
+ const lordStartDeg = startNak * NAK_SPAN;
30
+ const spanDeg = spanNak * NAK_SPAN;
31
+ const elapsed = (((lon - lordStartDeg) % 360) + 360) % 360 / spanDeg;
32
+ const y0 = ASHTOTTARI_YEARS[startLord];
33
+ const li = ASHTOTTARI_ORDER.indexOf(startLord);
34
+ let t = natalJd - elapsed * y0 * yearLength;
35
+ const dashas = [];
36
+ for (let k = 0; k < count; k++) {
37
+ const lord = ASHTOTTARI_ORDER[(li + k) % 8];
38
+ const years = ASHTOTTARI_YEARS[lord];
39
+ const span = years * yearLength;
40
+ const maha = { level: 1, lord, years, start: t, end: t + span, sub: [] };
41
+ if (levels >= 2) {
42
+ const sli = ASHTOTTARI_ORDER.indexOf(lord);
43
+ let st = t;
44
+ for (let j = 0; j < 8; j++) {
45
+ const sl = ASHTOTTARI_ORDER[(sli + j) % 8];
46
+ const subSpan = (years * ASHTOTTARI_YEARS[sl] / ASHTOTTARI_TOTAL) * yearLength;
47
+ maha.sub.push({ lord: sl, start: st, end: st + subSpan });
48
+ st += subSpan;
49
+ }
50
+ }
51
+ dashas.push(maha);
52
+ t += span;
53
+ }
54
+ return { start_lord: startLord, balance_years: (1 - elapsed) * y0, dashas };
55
+ }
56
+ /** The maha and antar lord active at targetJd; null before the first period. */
57
+ export function ashtottariActive(moonLon, natalJd, targetJd, yearLength = DASHA_YEAR) {
58
+ const timeline = ashtottariDashas(moonLon, natalJd, 2, yearLength, 16).dashas;
59
+ const maha = timeline.find((p) => p.start <= targetJd && targetJd < p.end);
60
+ if (!maha)
61
+ return null;
62
+ const antar = maha.sub.find((s) => s.start <= targetJd && targetJd < s.end);
63
+ return { maha: maha.lord, antar: antar ? antar.lord : null };
64
+ }
65
+ /** Ashtottari dasha active at targetJd, from the natal Moon's nakshatra. */
66
+ export function ashtottariAt(engine, natalJd, targetJd, zodiac = "sidereal:lahiri", yearLength = DASHA_YEAR) {
67
+ const moonLon = engine.longitude("moon", natalJd, { zodiac });
68
+ const nak = nakshatra(moonLon);
69
+ const active = ashtottariActive(moonLon, natalJd, targetJd, yearLength) ?? {};
70
+ return { moon_nakshatra: nak.name, start_lord: ashtottariLord(nak.index), ...active };
71
+ }
@@ -15,6 +15,21 @@ export interface DirectionArcs {
15
15
  export declare function directionArcs(alpha: number, delta: number, ramc: number, phi: number): DirectionArcs;
16
16
  /** Years of life corresponding to an arc of direction under a time key. */
17
17
  export declare function directionYears(arc: number, key?: string): number;
18
+ /** Placidus semi-arc mundane directional arc (degrees) for promissor P to
19
+ * significator S: arc = MD_p - (MD_s / SA_s) * SA_p. null if either body is
20
+ * circumpolar. Reduces to the to-MC arc when S is on the meridian, and to 0
21
+ * when P and S coincide. */
22
+ export declare function mundaneDirectionArc(alphaP: number, deltaP: number, alphaS: number, deltaS: number, ramc: number, phi: number): number | null;
23
+ export interface MundaneDirection {
24
+ promissor: string;
25
+ significator: string;
26
+ arc: number;
27
+ years: number;
28
+ jd: number;
29
+ }
30
+ /** Direct mundane (Placidus semi-arc) directions of each promissor to each other
31
+ * significator within `maxYears`, sorted by years. */
32
+ export declare function mundaneDirections(engine: Engine, natalJd: number, lat: number, lonEast: number, bodies?: BodyId[], key?: string, maxYears?: number, yearLength?: number): MundaneDirection[];
18
33
  export interface PrimaryDirection {
19
34
  body: string;
20
35
  angle: "MC" | "IC" | "ASC" | "DSC";
@@ -44,6 +44,56 @@ export function directionArcs(alpha, delta, ramc, phi) {
44
44
  export function directionYears(arc, key = "naibod") {
45
45
  return arc / KEYS[key];
46
46
  }
47
+ /** A body's signed meridian distance from its nearest meridian and the matching
48
+ * semi-arc, [MD, SA] in degrees; null when circumpolar. */
49
+ function semiArcPosition(alpha, delta, ramc, phi) {
50
+ const t = Math.tan(phi * RAD) * Math.tan(delta * RAD);
51
+ if (Math.abs(t) > 1)
52
+ return null;
53
+ const ad = Math.asin(t) * DEG;
54
+ const mdu = ((alpha - ramc + 180) % 360 + 360) % 360 - 180; // upper meridian distance
55
+ if (Math.abs(mdu) <= 90 + ad)
56
+ return [mdu, 90 + ad];
57
+ const sign = mdu >= 0 ? 1 : -1;
58
+ return [sign * (180 - Math.abs(mdu)), 90 - ad];
59
+ }
60
+ /** Placidus semi-arc mundane directional arc (degrees) for promissor P to
61
+ * significator S: arc = MD_p - (MD_s / SA_s) * SA_p. null if either body is
62
+ * circumpolar. Reduces to the to-MC arc when S is on the meridian, and to 0
63
+ * when P and S coincide. */
64
+ export function mundaneDirectionArc(alphaP, deltaP, alphaS, deltaS, ramc, phi) {
65
+ const pp = semiArcPosition(alphaP, deltaP, ramc, phi);
66
+ const ps = semiArcPosition(alphaS, deltaS, ramc, phi);
67
+ if (pp === null || ps === null)
68
+ return null;
69
+ const [mdP, saP] = pp;
70
+ const [mdS, saS] = ps;
71
+ return mdP - (mdS / saS) * saP;
72
+ }
73
+ /** Direct mundane (Placidus semi-arc) directions of each promissor to each other
74
+ * significator within `maxYears`, sorted by years. */
75
+ export function mundaneDirections(engine, natalJd, lat, lonEast, bodies = TRADITIONAL, key = "naibod", maxYears = 90, yearLength = YEAR_DAYS) {
76
+ const ramc = angles(engine.data, natalJd, lat, lonEast)[2] * DEG;
77
+ const pos = {};
78
+ for (const b of bodies)
79
+ pos[b] = engine.position(b, natalJd);
80
+ const out = [];
81
+ for (const p of bodies) {
82
+ for (const s of bodies) {
83
+ if (p === s)
84
+ continue;
85
+ const arc = mundaneDirectionArc(pos[p].ra, pos[p].dec, pos[s].ra, pos[s].dec, ramc, lat);
86
+ if (arc === null)
87
+ continue;
88
+ const years = directionYears(arc, key);
89
+ if (years >= 0 && years <= maxYears) {
90
+ out.push({ promissor: p, significator: s, arc, years, jd: natalJd + years * yearLength });
91
+ }
92
+ }
93
+ }
94
+ out.sort((a, b) => a.years - b.years);
95
+ return out;
96
+ }
47
97
  /** Direct primary directions of the bodies to the four angles within
48
98
  * `maxYears`, by the given time key, sorted by years. */
49
99
  export function primaryDirections(engine, natalJd, lat, lonEast, bodies = TRADITIONAL, key = "naibod", maxYears = 90, yearLength = YEAR_DAYS) {
@@ -24,3 +24,5 @@ export * from "./directions.js";
24
24
  export * from "./vargas.js";
25
25
  export * from "./yogini.js";
26
26
  export * from "./yogas.js";
27
+ export * from "./ashtottari.js";
28
+ export * from "./rajayoga.js";
package/dist/src/index.js CHANGED
@@ -24,3 +24,5 @@ export * from "./directions.js";
24
24
  export * from "./vargas.js";
25
25
  export * from "./yogini.js";
26
26
  export * from "./yogas.js";
27
+ export * from "./ashtottari.js";
28
+ export * from "./rajayoga.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * astroengine rajayoga -- the lordship-and-aspect layer for Vedic yogas, and the
3
+ * raja/dhana yogas built on it.
4
+ *
5
+ * Layers: house lordship (traditional ruler of each whole-sign house from the
6
+ * Ascendant); graha drishti (every planet aspects the 7th sign; Mars also 4/8,
7
+ * Jupiter 5/9, Saturn 3/10); association (conjunction, mutual aspect, or
8
+ * parivartana exchange); then raja yoga (a kendra lord 1/4/7/10 associated with
9
+ * a trikona lord 1/5/9), dhana yoga (two wealth-house 2/5/9/11 lords), and the
10
+ * yogakaraka (a planet ruling both a pure kendra 4/7/10 and a pure trikona 5/9).
11
+ * Definitions follow BPHS, validated against the named source in
12
+ * `validate_jyotish`. Mirrors the Python reference (astroengine/rajayoga.py).
13
+ */
14
+ import { Engine, Zodiac } from "./chart.js";
15
+ /** Graha drishti: the house-distances (1-based) each planet aspects. */
16
+ export declare const DRISHTI: Record<string, number[]>;
17
+ export declare const KENDRAS: number[];
18
+ export declare const TRIKONAS: number[];
19
+ export declare const DHANA_HOUSES: number[];
20
+ /** The traditional ruler of a sign index (0 = Aries). */
21
+ export declare function signLord(sign: number): string;
22
+ /** The sign on the given whole-sign house (1-12) from the Ascendant. */
23
+ export declare function houseSign(ascSign: number, house: number): number;
24
+ /** The lord of the given whole-sign house from the Ascendant. */
25
+ export declare function houseLord(ascSign: number, house: number): string;
26
+ /** The whole-sign house (1-12) a sign falls in from the Ascendant. */
27
+ export declare function houseFromAsc(ascSign: number, sign: number): number;
28
+ /** Whether `planet` at `planetSign` casts a graha drishti onto `targetSign`. */
29
+ export declare function aspectsSign(planet: string, planetSign: number, targetSign: number): boolean;
30
+ /** Sign exchange: a sits in b's sign and b sits in a's sign. */
31
+ export declare function parivartana(planetA: string, signA: number, planetB: string, signB: number): boolean;
32
+ /** How two planets associate: "conjunction" | "exchange" | "aspect" | null. */
33
+ export declare function associationType(planetA: string, signA: number, planetB: string, signB: number): string | null;
34
+ /** Planets ruling both a pure kendra (4/7/10) and a pure trikona (5/9), sorted. */
35
+ export declare function yogakarakas(ascSign: number): string[];
36
+ export interface LordPairYoga {
37
+ lords: string[];
38
+ via: string;
39
+ }
40
+ /** Raja yogas: associations between a kendra lord and a trikona lord. */
41
+ export declare function rajaYogas(signs: Record<string, number>, ascSign: number): LordPairYoga[];
42
+ /** Dhana yogas: associations between two wealth-house (2/5/9/11) lords. */
43
+ export declare function dhanaYogas(signs: Record<string, number>, ascSign: number): LordPairYoga[];
44
+ /** Raja yogas of a natal chart, with the chart's yogakarakas. */
45
+ export declare function rajaYogasAt(engine: Engine, natalJd: number, lat: number, lonEast: number, zodiac?: Zodiac): {
46
+ raja: LordPairYoga[];
47
+ yogakarakas: string[];
48
+ };
49
+ /** Dhana yogas of a natal chart. */
50
+ export declare function dhanaYogasAt(engine: Engine, natalJd: number, lat: number, lonEast: number, zodiac?: Zodiac): LordPairYoga[];
@@ -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
+ }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { Engine, BodyId, Zodiac } from "./chart.js";
17
17
  /** Supported divisions. */
18
- export declare const VARGA_DIVISIONS: readonly [1, 3, 9, 10, 12];
18
+ export declare const VARGA_DIVISIONS: readonly [1, 2, 3, 9, 10, 12, 30];
19
19
  export interface Varga {
20
20
  varga: number;
21
21
  rasi: string;
@@ -17,10 +17,26 @@ import { BODIES, SIGNS } from "./chart.js";
17
17
  /** Element start sign for the navamsa (fire, earth, air, water by rasi % 4). */
18
18
  const NAVAMSA_START = [0, 9, 6, 3];
19
19
  /** Supported divisions. */
20
- export const VARGA_DIVISIONS = [1, 3, 9, 10, 12];
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
+ }
21
34
  function vargaSign(rasi, div, n) {
22
35
  switch (n) {
23
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;
24
40
  case 3: return (rasi + 4 * div) % 12;
25
41
  case 9: return (NAVAMSA_START[rasi % 4] + div) % 12;
26
42
  case 10: return rasi % 2 === 0 ? (rasi + div) % 12 : (rasi + 8 + div) % 12;
@@ -33,11 +49,19 @@ export function varga(siderealLon, n) {
33
49
  const lon = ((siderealLon % 360) + 360) % 360;
34
50
  const rasi = Math.floor(lon / 30) % 12;
35
51
  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 };
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 };
41
65
  }
42
66
  /** The varga D-n of a body (default the Moon) at jd, in a sidereal zodiac. */
43
67
  export function vargaAt(engine, jdUt, n, body = "moon", zodiac = "sidereal:lahiri") {
@@ -22,5 +22,16 @@ export interface Yoga {
22
22
  * classical planets to its 0-based sign index; `ascSign` is the Ascendant's
23
23
  * sign index. */
24
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;
25
36
  /** The placement yogas of a natal chart, from the sidereal rasi positions. */
26
37
  export declare function yogasAt(engine: Engine, natalJd: number, lat: number, lonEast: number, zodiac?: Zodiac): Yoga[];
package/dist/src/yogas.js CHANGED
@@ -28,6 +28,35 @@ export function detectYogas(signs, ascSign) {
28
28
  out.push({ yoga: "Chandra-Mangala", planets: ["moon", "mars"] });
29
29
  return out;
30
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
+ }
31
60
  /** The placement yogas of a natal chart, from the sidereal rasi positions. */
32
61
  export function yogasAt(engine, natalJd, lat, lonEast, zodiac = "sidereal:lahiri") {
33
62
  const chart = engine.chartAt(natalJd, lat, lonEast, { zodiac });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caelus",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",
@@ -21,6 +21,10 @@
21
21
  "test": "node dist/test/golden.test.js"
22
22
  },
23
23
  "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "provenance": true
27
+ },
24
28
  "devDependencies": {
25
29
  "@types/node": "^25.9.2",
26
30
  "typescript": "^6.0.3"