caelus 0.14.0 → 0.16.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 +1 -1
- package/accuracy.json +1 -1
- package/dist/src/chart.d.ts +80 -9
- package/dist/src/chart.js +148 -16
- package/dist/src/derived.d.ts +0 -15
- package/dist/src/derived.js +4 -39
- package/dist/src/dignity-score.d.ts +50 -0
- package/dist/src/dignity-score.js +122 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/parans.d.ts +27 -0
- package/dist/src/parans.js +59 -0
- package/dist/src/patterns.d.ts +52 -0
- package/dist/src/patterns.js +224 -0
- package/dist/src/rajayoga.js +1 -0
- package/dist/src/signature.d.ts +58 -0
- package/dist/src/signature.js +106 -0
- package/dist/src/yogas.d.ts +2 -2
- package/dist/src/yogas.js +16 -1
- package/package.json +1 -1
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, twenty-
|
|
103
|
+
- [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, twenty-seven chart tools over stdio
|
package/accuracy.json
CHANGED
package/dist/src/chart.d.ts
CHANGED
|
@@ -6,10 +6,45 @@ export type Body = (typeof BODIES)[number];
|
|
|
6
6
|
export declare const EXTRA_BODIES: readonly ["mean_lilith", "true_lilith"];
|
|
7
7
|
/** Core names keep autocomplete; any string id is accepted (data packs). */
|
|
8
8
|
export type BodyId = Body | (typeof EXTRA_BODIES)[number] | (string & {});
|
|
9
|
+
/** Points: excluded from aspect search by default. */
|
|
10
|
+
export declare const NOT_ASPECTABLE: Set<string>;
|
|
9
11
|
export declare const SIGNS: string[];
|
|
10
12
|
export declare const ASPECTS: Record<string, number>;
|
|
11
13
|
export declare const DEFAULT_ORBS: Record<string, number>;
|
|
12
14
|
export type HouseSystem = "placidus" | "porphyry" | "equal" | "whole_sign" | "koch" | "regiomontanus" | "campanus" | "alcabitius" | "morinus" | "meridian" | "polich_page" | "vehlow";
|
|
15
|
+
/** The canonical house-system ids, in a stable order (also used for error text). */
|
|
16
|
+
export declare const HOUSE_SYSTEMS: readonly HouseSystem[];
|
|
17
|
+
/** Resolve a forgiving house-system string (any case, spaces or hyphens, or a
|
|
18
|
+
* known alias) to a canonical {@link HouseSystem}, or throw listing the valid
|
|
19
|
+
* ids. Lets MCP, share links, and hand-written calls pass "whole sign",
|
|
20
|
+
* "Whole_Sign", "whole", etc. without tripping the strict union. */
|
|
21
|
+
export declare function normalizeHouseSystem(raw: string): HouseSystem;
|
|
22
|
+
export type Element = "fire" | "earth" | "air" | "water";
|
|
23
|
+
export type Modality = "cardinal" | "fixed" | "mutable";
|
|
24
|
+
/** Triplicity (element) of a sign: `"fire"`, `"earth"`, `"air"`, or `"water"`. */
|
|
25
|
+
export declare function element(sign: number | string): Element;
|
|
26
|
+
/** Quadruplicity (modality) of a sign: `"cardinal"`, `"fixed"`, or `"mutable"`. */
|
|
27
|
+
export declare function modality(sign: number | string): Modality;
|
|
28
|
+
/** 1-based quadrant (I–IV) of a 1-based house number: houses 1–3 -> 1, etc. */
|
|
29
|
+
export declare function quadrant(house: number): number;
|
|
30
|
+
export declare const DOMICILE: Record<string, number[]>;
|
|
31
|
+
export declare const EXALTATION: Record<string, number>;
|
|
32
|
+
/**
|
|
33
|
+
* Essential dignities a body holds in a sign: any of `"domicile"`,
|
|
34
|
+
* `"exaltation"`, `"detriment"`, `"fall"` (the last two are the signs opposite
|
|
35
|
+
* domicile and exaltation). Empty when the body is peregrine there or has no
|
|
36
|
+
* classical rulership (the outer planets, Chiron, the nodes).
|
|
37
|
+
*
|
|
38
|
+
* @param body Body id, e.g. `"mars"`.
|
|
39
|
+
* @param sign A sign index `0`–`11` (Aries = 0) or its name, e.g. `"Aries"`.
|
|
40
|
+
* @returns The dignities held, in the order above; empty if none.
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* dignities("mars", "Aries"); // ["domicile"]
|
|
44
|
+
* dignities("sun", "Libra"); // ["fall"]
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function dignities(body: string, sign: number | string): string[];
|
|
13
48
|
export type Ayanamsa = keyof typeof AYANAMSA_J2000 & string;
|
|
14
49
|
export type Zodiac = "tropical" | `sidereal:${string}`;
|
|
15
50
|
export interface Observer {
|
|
@@ -58,6 +93,33 @@ export interface Position {
|
|
|
58
93
|
/** Equatorial declination, true equinox of date, degrees. */
|
|
59
94
|
dec: number;
|
|
60
95
|
}
|
|
96
|
+
/** A {@link Position} enriched with chart-relative placement, as returned per
|
|
97
|
+
* body by {@link Engine.chart} and {@link Engine.chartAt}. */
|
|
98
|
+
export interface ChartBody extends Position {
|
|
99
|
+
/** 1-based house the body falls in, by the chart's cusps (1–12). */
|
|
100
|
+
house: number;
|
|
101
|
+
/** Essential dignities held in the body's sign (see {@link dignities});
|
|
102
|
+
* empty when peregrine or for bodies without classical rulerships. */
|
|
103
|
+
dignities: string[];
|
|
104
|
+
}
|
|
105
|
+
/** Default chart bodies that are Chebyshev-packed, so they can fall outside
|
|
106
|
+
* their fitted range (and be omitted from a chart). Opt-in asteroids are
|
|
107
|
+
* packed too, but arrive as arbitrary ids through the string index. */
|
|
108
|
+
export type PackedBody = "chiron";
|
|
109
|
+
/** Bodies guaranteed to be in every chart: the analytic Sun–Pluto and the lunar
|
|
110
|
+
* nodes, which resolve across all supported epochs. (Chiron is Chebyshev-packed
|
|
111
|
+
* and can fall outside its fitted range, so it is *not* guaranteed.) */
|
|
112
|
+
export type AlwaysBody = Exclude<Body, PackedBody>;
|
|
113
|
+
/**
|
|
114
|
+
* A chart's bodies, keyed by id. The analytic core ({@link AlwaysBody}) is
|
|
115
|
+
* always present and needs no presence check. {@link PackedBody} bodies (Chiron)
|
|
116
|
+
* and any opt-in extras requested via {@link ChartOptions.bodies} may be absent
|
|
117
|
+
* when the instant is outside their fitted range (see {@link Chart.unavailable}),
|
|
118
|
+
* so those accesses are typed `ChartBody | undefined` and must be guarded.
|
|
119
|
+
*/
|
|
120
|
+
export type ChartBodies = Record<AlwaysBody, ChartBody> & Partial<Record<PackedBody, ChartBody>> & {
|
|
121
|
+
[id: string]: ChartBody | undefined;
|
|
122
|
+
};
|
|
61
123
|
/** One aspect between two bodies in a {@link Chart}. */
|
|
62
124
|
export interface Aspect {
|
|
63
125
|
/** First body id. */
|
|
@@ -81,8 +143,14 @@ export interface Chart {
|
|
|
81
143
|
houseSystem: HouseSystem;
|
|
82
144
|
/** The house system originally requested, before any polar fallback. */
|
|
83
145
|
houseSystemRequested: HouseSystem;
|
|
84
|
-
/** Apparent
|
|
85
|
-
|
|
146
|
+
/** Apparent position per body, enriched with house and dignities, keyed by
|
|
147
|
+
* body id. See {@link ChartBody}. */
|
|
148
|
+
bodies: ChartBodies;
|
|
149
|
+
/** Body ids that were requested but omitted because the instant falls outside
|
|
150
|
+
* their fitted range (e.g. Chiron and other Chebyshev-packed bodies before
|
|
151
|
+
* ~1850 or after ~2150). Empty for the usual modern dates. The analytic
|
|
152
|
+
* bodies (Sun through Pluto and the nodes) are always present. */
|
|
153
|
+
unavailable: string[];
|
|
86
154
|
/** Chart angles in degrees: Ascendant, Midheaven, Vertex, East Point. */
|
|
87
155
|
angles: {
|
|
88
156
|
asc: number;
|
|
@@ -235,10 +303,10 @@ export declare class Engine {
|
|
|
235
303
|
* instant and place.
|
|
236
304
|
*
|
|
237
305
|
* The first six arguments are calendar fields in **UT** — not local civil
|
|
238
|
-
* time, and not a Julian Day. Passing a JD in `y` builds an instant
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
306
|
+
* time, and not a Julian Day. Passing a JD in `y` builds an absurd instant and
|
|
307
|
+
* throws `RangeError`; use {@link Engine.chartAt} for a chart from a JD. For a
|
|
308
|
+
* birth time given in a local time zone, resolve it to UT first (see the
|
|
309
|
+
* `caelus-birth` package).
|
|
242
310
|
*
|
|
243
311
|
* @param y Year in UT, e.g. `1990` — a calendar year, not a Julian Day.
|
|
244
312
|
* @param mo Month, `1`–`12`.
|
|
@@ -254,9 +322,12 @@ export declare class Engine {
|
|
|
254
322
|
* custom orbs. Defaults to Placidus houses in the tropical zodiac.
|
|
255
323
|
* @returns A {@link Chart}: `bodies`, `cusps`, `angles`, and `aspects`, plus
|
|
256
324
|
* `jdUt` and the house system actually used (Placidus and Koch fall back to
|
|
257
|
-
* whole-sign above the polar circles).
|
|
258
|
-
*
|
|
259
|
-
*
|
|
325
|
+
* whole-sign above the polar circles). A body outside its fitted range
|
|
326
|
+
* (e.g. Chiron before ~1850) is omitted from `bodies` and listed in
|
|
327
|
+
* `unavailable` rather than failing the whole chart.
|
|
328
|
+
* @throws RangeError only if the instant itself is absurd — far outside any
|
|
329
|
+
* supported epoch — which almost always means a Julian Day was passed where
|
|
330
|
+
* calendar fields belong.
|
|
260
331
|
* @example
|
|
261
332
|
* ```ts
|
|
262
333
|
* // 1990-06-10 14:30 UT at Tampa, FL (27.95° N, 82.46° W), Placidus houses
|
package/dist/src/chart.js
CHANGED
|
@@ -10,7 +10,7 @@ export const BODIES = [
|
|
|
10
10
|
/** Computable on request (not in the default chart set). */
|
|
11
11
|
export const EXTRA_BODIES = ["mean_lilith", "true_lilith"];
|
|
12
12
|
/** Points: excluded from aspect search by default. */
|
|
13
|
-
const NOT_ASPECTABLE = new Set([
|
|
13
|
+
export const NOT_ASPECTABLE = new Set([
|
|
14
14
|
"mean_node", "true_node", "mean_lilith", "true_lilith",
|
|
15
15
|
]);
|
|
16
16
|
export const SIGNS = [
|
|
@@ -23,7 +23,92 @@ export const ASPECTS = {
|
|
|
23
23
|
export const DEFAULT_ORBS = {
|
|
24
24
|
conjunction: 8, sextile: 4, square: 7, trine: 7, opposition: 8,
|
|
25
25
|
};
|
|
26
|
+
/** The canonical house-system ids, in a stable order (also used for error text). */
|
|
27
|
+
export const HOUSE_SYSTEMS = [
|
|
28
|
+
"placidus", "porphyry", "equal", "whole_sign", "koch", "regiomontanus",
|
|
29
|
+
"campanus", "alcabitius", "morinus", "meridian", "polich_page", "vehlow",
|
|
30
|
+
];
|
|
31
|
+
// Short or alternate names that normalization (lowercase + space/hyphen -> "_")
|
|
32
|
+
// can't reach on its own. "whole sign", "Polich Page", etc. already normalize to
|
|
33
|
+
// their canonical id, so only genuinely different spellings live here.
|
|
34
|
+
const HOUSE_ALIASES = {
|
|
35
|
+
whole: "whole_sign", signs: "whole_sign", wholesign: "whole_sign",
|
|
36
|
+
equal_house: "equal", porphyrius: "porphyry", placidean: "placidus",
|
|
37
|
+
};
|
|
38
|
+
/** Resolve a forgiving house-system string (any case, spaces or hyphens, or a
|
|
39
|
+
* known alias) to a canonical {@link HouseSystem}, or throw listing the valid
|
|
40
|
+
* ids. Lets MCP, share links, and hand-written calls pass "whole sign",
|
|
41
|
+
* "Whole_Sign", "whole", etc. without tripping the strict union. */
|
|
42
|
+
export function normalizeHouseSystem(raw) {
|
|
43
|
+
const key = raw.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
44
|
+
if (HOUSE_SYSTEMS.includes(key))
|
|
45
|
+
return key;
|
|
46
|
+
const alias = HOUSE_ALIASES[key];
|
|
47
|
+
if (alias)
|
|
48
|
+
return alias;
|
|
49
|
+
throw new Error(`unknown house system '${raw}' (valid: ${HOUSE_SYSTEMS.join(", ")})`);
|
|
50
|
+
}
|
|
51
|
+
const ELEMENTS = ["fire", "earth", "air", "water"];
|
|
52
|
+
const MODALITIES = ["cardinal", "fixed", "mutable"];
|
|
53
|
+
/** Sign index `0`–`11` (Aries = 0) from an index or a sign name (`"Aries"`). */
|
|
54
|
+
function signIndex(sign) {
|
|
55
|
+
return typeof sign === "number" ? mod(Math.floor(sign), 12) : SIGNS.indexOf(sign);
|
|
56
|
+
}
|
|
57
|
+
/** Triplicity (element) of a sign: `"fire"`, `"earth"`, `"air"`, or `"water"`. */
|
|
58
|
+
export function element(sign) {
|
|
59
|
+
return ELEMENTS[mod(signIndex(sign), 4)];
|
|
60
|
+
}
|
|
61
|
+
/** Quadruplicity (modality) of a sign: `"cardinal"`, `"fixed"`, or `"mutable"`. */
|
|
62
|
+
export function modality(sign) {
|
|
63
|
+
return MODALITIES[mod(signIndex(sign), 3)];
|
|
64
|
+
}
|
|
65
|
+
/** 1-based quadrant (I–IV) of a 1-based house number: houses 1–3 -> 1, etc. */
|
|
66
|
+
export function quadrant(house) {
|
|
67
|
+
return Math.floor(mod(house - 1, 12) / 3) + 1;
|
|
68
|
+
}
|
|
69
|
+
// ----------------------------------------------------------- essential dignities
|
|
70
|
+
export const DOMICILE = {
|
|
71
|
+
sun: [4], moon: [3], mercury: [2, 5], venus: [1, 6],
|
|
72
|
+
mars: [0, 7], jupiter: [8, 11], saturn: [9, 10],
|
|
73
|
+
};
|
|
74
|
+
export const EXALTATION = {
|
|
75
|
+
sun: 0, moon: 1, mercury: 5, venus: 11, mars: 9, jupiter: 3, saturn: 6,
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Essential dignities a body holds in a sign: any of `"domicile"`,
|
|
79
|
+
* `"exaltation"`, `"detriment"`, `"fall"` (the last two are the signs opposite
|
|
80
|
+
* domicile and exaltation). Empty when the body is peregrine there or has no
|
|
81
|
+
* classical rulership (the outer planets, Chiron, the nodes).
|
|
82
|
+
*
|
|
83
|
+
* @param body Body id, e.g. `"mars"`.
|
|
84
|
+
* @param sign A sign index `0`–`11` (Aries = 0) or its name, e.g. `"Aries"`.
|
|
85
|
+
* @returns The dignities held, in the order above; empty if none.
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* dignities("mars", "Aries"); // ["domicile"]
|
|
89
|
+
* dignities("sun", "Libra"); // ["fall"]
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function dignities(body, sign) {
|
|
93
|
+
const idx = signIndex(sign);
|
|
94
|
+
const dom = DOMICILE[body] ?? [];
|
|
95
|
+
const out = [];
|
|
96
|
+
if (dom.includes(idx))
|
|
97
|
+
out.push("domicile");
|
|
98
|
+
if (EXALTATION[body] === idx)
|
|
99
|
+
out.push("exaltation");
|
|
100
|
+
if (dom.map((d) => mod(d + 6, 12)).includes(idx))
|
|
101
|
+
out.push("detriment");
|
|
102
|
+
if (body in EXALTATION && mod(EXALTATION[body] + 6, 12) === idx)
|
|
103
|
+
out.push("fall");
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
26
106
|
const KM_PER_AU = 149597870.7;
|
|
107
|
+
// A generous window for the analytic models. Every real chart, however
|
|
108
|
+
// historical, sits well inside it; an instant far outside almost always means a
|
|
109
|
+
// Julian Day was passed to chart() where calendar fields belong.
|
|
110
|
+
const JD_SANE_MIN = -2_000_000; // ~ 10000 BC
|
|
111
|
+
const JD_SANE_MAX = 9_000_000; // ~ 20000 AD
|
|
27
112
|
function parseZodiac(zodiac) {
|
|
28
113
|
if (zodiac === "tropical")
|
|
29
114
|
return null;
|
|
@@ -339,10 +424,10 @@ export class Engine {
|
|
|
339
424
|
* instant and place.
|
|
340
425
|
*
|
|
341
426
|
* The first six arguments are calendar fields in **UT** — not local civil
|
|
342
|
-
* time, and not a Julian Day. Passing a JD in `y` builds an instant
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
427
|
+
* time, and not a Julian Day. Passing a JD in `y` builds an absurd instant and
|
|
428
|
+
* throws `RangeError`; use {@link Engine.chartAt} for a chart from a JD. For a
|
|
429
|
+
* birth time given in a local time zone, resolve it to UT first (see the
|
|
430
|
+
* `caelus-birth` package).
|
|
346
431
|
*
|
|
347
432
|
* @param y Year in UT, e.g. `1990` — a calendar year, not a Julian Day.
|
|
348
433
|
* @param mo Month, `1`–`12`.
|
|
@@ -358,9 +443,12 @@ export class Engine {
|
|
|
358
443
|
* custom orbs. Defaults to Placidus houses in the tropical zodiac.
|
|
359
444
|
* @returns A {@link Chart}: `bodies`, `cusps`, `angles`, and `aspects`, plus
|
|
360
445
|
* `jdUt` and the house system actually used (Placidus and Koch fall back to
|
|
361
|
-
* whole-sign above the polar circles).
|
|
362
|
-
*
|
|
363
|
-
*
|
|
446
|
+
* whole-sign above the polar circles). A body outside its fitted range
|
|
447
|
+
* (e.g. Chiron before ~1850) is omitted from `bodies` and listed in
|
|
448
|
+
* `unavailable` rather than failing the whole chart.
|
|
449
|
+
* @throws RangeError only if the instant itself is absurd — far outside any
|
|
450
|
+
* supported epoch — which almost always means a Julian Day was passed where
|
|
451
|
+
* calendar fields belong.
|
|
364
452
|
* @example
|
|
365
453
|
* ```ts
|
|
366
454
|
* // 1990-06-10 14:30 UT at Tampa, FL (27.95° N, 82.46° W), Placidus houses
|
|
@@ -393,8 +481,12 @@ export class Engine {
|
|
|
393
481
|
* @see {@link Engine.chart} for the calendar-field entry point.
|
|
394
482
|
*/
|
|
395
483
|
chartAt(jdUt, lat, lonEast, opts = "placidus") {
|
|
484
|
+
if (!Number.isFinite(jdUt) || jdUt < JD_SANE_MIN || jdUt > JD_SANE_MAX) {
|
|
485
|
+
throw new RangeError(`chart instant (jd ${jdUt}) is far outside the supported range; if you ` +
|
|
486
|
+
`meant a calendar date, pass year/month/day to chart() rather than a Julian Day.`);
|
|
487
|
+
}
|
|
396
488
|
const o = typeof opts === "string" ? { houseSystem: opts } : opts;
|
|
397
|
-
const houseSystem = o.houseSystem ?? "placidus";
|
|
489
|
+
const houseSystem = normalizeHouseSystem(o.houseSystem ?? "placidus");
|
|
398
490
|
const zodiac = o.zodiac ?? "tropical";
|
|
399
491
|
const mode = parseZodiac(zodiac);
|
|
400
492
|
const calc = {
|
|
@@ -406,8 +498,22 @@ export class Engine {
|
|
|
406
498
|
...BODIES, ...(o.bodies ?? []).filter((b) => !BODIES.includes(b)),
|
|
407
499
|
];
|
|
408
500
|
const bodies = {};
|
|
409
|
-
|
|
410
|
-
|
|
501
|
+
const unavailable = [];
|
|
502
|
+
for (const b of names) {
|
|
503
|
+
try {
|
|
504
|
+
bodies[b] = this.position(b, jdUt, calc);
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
// A Chebyshev-packed body (Chiron, fitted asteroids) outside its fitted
|
|
508
|
+
// range throws RangeError. Omit it and report it rather than discarding
|
|
509
|
+
// the whole chart; the analytic bodies still resolve. Any other error
|
|
510
|
+
// (e.g. a missing data pack) is a real fault and propagates.
|
|
511
|
+
if (e instanceof RangeError)
|
|
512
|
+
unavailable.push(b);
|
|
513
|
+
else
|
|
514
|
+
throw e;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
411
517
|
const [asc, mc, armc, eps] = H.angles(this.data, jdUt, lat, lonEast);
|
|
412
518
|
const [vtx, east] = H.vertexEastPoint(armc, lat * DEG, eps);
|
|
413
519
|
const phi = lat * DEG;
|
|
@@ -454,7 +560,7 @@ export class Engine {
|
|
|
454
560
|
cusps = H.housesVehlow(armc, phi, eps);
|
|
455
561
|
}
|
|
456
562
|
else {
|
|
457
|
-
throw new Error(`unknown house system '${houseSystem}'`);
|
|
563
|
+
throw new Error(`unknown house system '${houseSystem}' (valid: ${HOUSE_SYSTEMS.join(", ")})`);
|
|
458
564
|
}
|
|
459
565
|
}
|
|
460
566
|
catch (err) {
|
|
@@ -477,21 +583,44 @@ export class Engine {
|
|
|
477
583
|
else {
|
|
478
584
|
cuspsDeg = cusps.map(outDeg);
|
|
479
585
|
}
|
|
586
|
+
// Enrich each position with chart-relative placement (house) and the
|
|
587
|
+
// essential dignities of its sign, so callers don't recompute from cusps.
|
|
588
|
+
const chartBodies = {};
|
|
589
|
+
for (const b of names) {
|
|
590
|
+
const p = bodies[b];
|
|
591
|
+
if (!p)
|
|
592
|
+
continue; // omitted: outside its fitted range (see `unavailable`)
|
|
593
|
+
chartBodies[b] = {
|
|
594
|
+
...p,
|
|
595
|
+
house: houseIndex(p.lon, cuspsDeg),
|
|
596
|
+
dignities: dignities(b, Math.floor(mod(p.lon, 360) / 30)),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
480
599
|
return {
|
|
481
600
|
jdUt,
|
|
482
601
|
zodiac,
|
|
483
602
|
houseSystem: used,
|
|
484
603
|
houseSystemRequested: houseSystem,
|
|
485
|
-
bodies,
|
|
604
|
+
bodies: chartBodies,
|
|
605
|
+
unavailable,
|
|
486
606
|
angles: {
|
|
487
607
|
asc: outDeg(asc), mc: outDeg(mc),
|
|
488
608
|
vertex: outDeg(vtx), eastPoint: outDeg(east),
|
|
489
609
|
},
|
|
490
610
|
cusps: cuspsDeg,
|
|
491
|
-
aspects: findAspects(
|
|
611
|
+
aspects: findAspects(chartBodies, o.orbs ?? DEFAULT_ORBS),
|
|
492
612
|
};
|
|
493
613
|
}
|
|
494
614
|
}
|
|
615
|
+
/** 1-based house for an ecliptic longitude (degrees) given the twelve cusp
|
|
616
|
+
* longitudes (degrees), wrapping across 0. */
|
|
617
|
+
function houseIndex(lon, cusps) {
|
|
618
|
+
for (let i = 0; i < 12; i++) {
|
|
619
|
+
if (mod(lon - cusps[i], 360) < mod(cusps[(i + 1) % 12] - cusps[i], 360))
|
|
620
|
+
return i + 1;
|
|
621
|
+
}
|
|
622
|
+
return 12;
|
|
623
|
+
}
|
|
495
624
|
export function findAspects(bodies, orbs = DEFAULT_ORBS) {
|
|
496
625
|
const out = [];
|
|
497
626
|
const names = Object.keys(bodies).filter((b) => !NOT_ASPECTABLE.has(b));
|
|
@@ -511,8 +640,11 @@ export function findAspects(bodies, orbs = DEFAULT_ORBS) {
|
|
|
511
640
|
return out;
|
|
512
641
|
}
|
|
513
642
|
export function fmtLon(deg) {
|
|
514
|
-
|
|
515
|
-
|
|
643
|
+
// Normalize first: a raw or rounded longitude (e.g. exactly 360, or a small
|
|
644
|
+
// negative) would otherwise index SIGNS out of range and render "undefined".
|
|
645
|
+
const norm = mod(deg, 360);
|
|
646
|
+
const sign = SIGNS[Math.floor(norm / 30)];
|
|
647
|
+
const d = mod(norm, 30);
|
|
516
648
|
const m = mod(d, 1) * 60;
|
|
517
649
|
return `${String(Math.floor(d)).padStart(2)}°${String(Math.floor(m)).padStart(2, "0")}' ${sign}`;
|
|
518
650
|
}
|
package/dist/src/derived.d.ts
CHANGED
|
@@ -109,21 +109,6 @@ export declare function declinationAspects(engine: Engine, bodies: BodyId[], jd:
|
|
|
109
109
|
/** |declination| minus the mean obliquity, degrees. Positive = out of bounds. */
|
|
110
110
|
export declare function outOfBoundsMargin(engine: Engine, body: BodyId, jd: number): number;
|
|
111
111
|
export declare function outOfBounds(engine: Engine, body: BodyId, jd: number): boolean;
|
|
112
|
-
/**
|
|
113
|
-
* Essential dignities a body holds in a sign: any of `"domicile"`,
|
|
114
|
-
* `"exaltation"`, `"detriment"`, `"fall"` (the last two are the signs opposite
|
|
115
|
-
* domicile and exaltation). Empty when the body is peregrine there.
|
|
116
|
-
*
|
|
117
|
-
* @param body Body id, e.g. `"mars"`.
|
|
118
|
-
* @param sign A sign index `0`–`11` (Aries = 0) or its name, e.g. `"Aries"`.
|
|
119
|
-
* @returns The dignities held, in the order above; empty if none.
|
|
120
|
-
* @example
|
|
121
|
-
* ```ts
|
|
122
|
-
* dignities("mars", "Aries"); // ["domicile"]
|
|
123
|
-
* dignities("sun", "Libra"); // ["fall"]
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
|
-
export declare function dignities(body: string, sign: number | string): string[];
|
|
127
112
|
export declare function dignityOf(engine: Engine, body: BodyId, jd: number, zodiac?: Zodiac): string[];
|
|
128
113
|
/** Diurnal when the Sun is above the horizon at the given place. */
|
|
129
114
|
export declare function isDayChart(engine: Engine, jd: number, lat: number, lonEast: number): boolean;
|
package/dist/src/derived.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* golden fixtures pin the two together.
|
|
10
10
|
*/
|
|
11
11
|
import { mod, meanObliquity, jdTT, DEG } from "./core.js";
|
|
12
|
-
import {
|
|
12
|
+
import { dignities } from "./chart.js";
|
|
13
13
|
import { crossings } from "./events.js";
|
|
14
14
|
import { azAlt } from "./pheno.js";
|
|
15
15
|
export const TROPICAL_YEAR = 365.24219; // mean tropical year, days
|
|
@@ -198,44 +198,9 @@ export function outOfBounds(engine, body, jd) {
|
|
|
198
198
|
return outOfBoundsMargin(engine, body, jd) > 0;
|
|
199
199
|
}
|
|
200
200
|
// ----------------------------------------------------------- dignities
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
};
|
|
205
|
-
const EXALTATION = {
|
|
206
|
-
sun: 0, moon: 1, mercury: 5, venus: 11, mars: 9, jupiter: 3, saturn: 6,
|
|
207
|
-
};
|
|
208
|
-
function signIndex(sign) {
|
|
209
|
-
return typeof sign === "number" ? sign : SIGNS.indexOf(sign);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Essential dignities a body holds in a sign: any of `"domicile"`,
|
|
213
|
-
* `"exaltation"`, `"detriment"`, `"fall"` (the last two are the signs opposite
|
|
214
|
-
* domicile and exaltation). Empty when the body is peregrine there.
|
|
215
|
-
*
|
|
216
|
-
* @param body Body id, e.g. `"mars"`.
|
|
217
|
-
* @param sign A sign index `0`–`11` (Aries = 0) or its name, e.g. `"Aries"`.
|
|
218
|
-
* @returns The dignities held, in the order above; empty if none.
|
|
219
|
-
* @example
|
|
220
|
-
* ```ts
|
|
221
|
-
* dignities("mars", "Aries"); // ["domicile"]
|
|
222
|
-
* dignities("sun", "Libra"); // ["fall"]
|
|
223
|
-
* ```
|
|
224
|
-
*/
|
|
225
|
-
export function dignities(body, sign) {
|
|
226
|
-
const idx = signIndex(sign);
|
|
227
|
-
const dom = DOMICILE[body] ?? [];
|
|
228
|
-
const out = [];
|
|
229
|
-
if (dom.includes(idx))
|
|
230
|
-
out.push("domicile");
|
|
231
|
-
if (EXALTATION[body] === idx)
|
|
232
|
-
out.push("exaltation");
|
|
233
|
-
if (dom.map((d) => mod(d + 6, 12)).includes(idx))
|
|
234
|
-
out.push("detriment");
|
|
235
|
-
if (body in EXALTATION && mod(EXALTATION[body] + 6, 12) === idx)
|
|
236
|
-
out.push("fall");
|
|
237
|
-
return out;
|
|
238
|
-
}
|
|
201
|
+
// The pure `dignities(body, sign)` table now lives in chart.ts (with the other
|
|
202
|
+
// sign primitives); re-exported through the package index from there. This
|
|
203
|
+
// engine-aware wrapper resolves a body's sign at an instant first.
|
|
239
204
|
export function dignityOf(engine, body, jd, zodiac = "tropical") {
|
|
240
205
|
const lon = engine.longitude(body, jd, { zodiac });
|
|
241
206
|
return dignities(body, mod(Math.floor(lon / 30), 12));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const PLANETS: string[];
|
|
2
|
+
export declare const DIGNITY_WEIGHTS: Record<string, number>;
|
|
3
|
+
/** Dorothean triplicity rulers by element (sign%4), as [day, night, participating]. */
|
|
4
|
+
export declare const TRIPLICITY: string[][];
|
|
5
|
+
/** Egyptian terms (Ptolemy I.21): per sign, [ruler, upper-degree] segments. */
|
|
6
|
+
export declare const TERMS_EGYPTIAN: Array<Array<[string, number]>>;
|
|
7
|
+
/** Faces (decans): Chaldean order from Mars at 0 Aries; floor(lon/10) selects. */
|
|
8
|
+
export declare const FACE_CYCLE: string[];
|
|
9
|
+
export type Sect = "day" | "night";
|
|
10
|
+
export declare function termRuler(sign: number, degInSign: number, terms?: [string, number][][]): string;
|
|
11
|
+
export declare function faceRuler(lon: number): string;
|
|
12
|
+
/** The essential-dignity breakdown of a planet at a longitude. */
|
|
13
|
+
export interface DignityScore {
|
|
14
|
+
planet: string;
|
|
15
|
+
rulership: number;
|
|
16
|
+
exaltation: number;
|
|
17
|
+
triplicity: number;
|
|
18
|
+
term: number;
|
|
19
|
+
face: number;
|
|
20
|
+
detriment: number;
|
|
21
|
+
fall: number;
|
|
22
|
+
/** Pure dignity sum (peregrine is not auto-scored). */
|
|
23
|
+
total: number;
|
|
24
|
+
/** True when none of the five dignities is held. */
|
|
25
|
+
peregrine: boolean;
|
|
26
|
+
term_ruler: string;
|
|
27
|
+
face_ruler: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The weighted essential dignities of `planet` at ecliptic longitude `lon`
|
|
31
|
+
* (degrees) in a day or night chart. Only the seven classical planets score.
|
|
32
|
+
*
|
|
33
|
+
* @param planet One of {@link PLANETS}.
|
|
34
|
+
* @param lon Ecliptic longitude in degrees.
|
|
35
|
+
* @param sect `"day"` or `"night"`, selecting the triplicity ruler.
|
|
36
|
+
* @param terms A term table; defaults to the Egyptian bounds.
|
|
37
|
+
* @returns A {@link DignityScore}.
|
|
38
|
+
*/
|
|
39
|
+
export declare function dignityScore(planet: string, lon: number, sect?: Sect, terms?: [string, number][][]): DignityScore;
|
|
40
|
+
/**
|
|
41
|
+
* The almuten of a degree: the classical planet with the greatest positive
|
|
42
|
+
* essential dignity (rulership + exaltation + triplicity + term + face) at `lon`.
|
|
43
|
+
* Ties broken by the canonical planet order.
|
|
44
|
+
*
|
|
45
|
+
* @returns `{ planet, score }`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function almuten(lon: number, sect?: Sect, terms?: [string, number][][]): {
|
|
48
|
+
planet: string;
|
|
49
|
+
score: number;
|
|
50
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weighted essential dignities (Ptolemaic / Lilly).
|
|
3
|
+
*
|
|
4
|
+
* Extends the qualitative {@link dignities} to the five-fold essential-dignity
|
|
5
|
+
* scoring of traditional astrology, with William Lilly's classical weights
|
|
6
|
+
* (*Christian Astrology*, 1647): rulership +5, exaltation +4, triplicity +3,
|
|
7
|
+
* term +2, face +1; detriment -5, fall -4.
|
|
8
|
+
*
|
|
9
|
+
* Tables, each pinned to a named authority and selectable where they vary:
|
|
10
|
+
* triplicity by the Dorothean rulers (day / night / participating); terms by the
|
|
11
|
+
* Egyptian bounds (Ptolemy, *Tetrabiblos* I.21); faces by the Chaldean order
|
|
12
|
+
* from 0 Aries. Peregrine (none of the five dignities present) is reported as a
|
|
13
|
+
* flag, not auto-scored, so `total` is a pure dignity sum. Port of the Python
|
|
14
|
+
* reference `astroengine.dignity_score`, pinned by `dignity-golden`.
|
|
15
|
+
*/
|
|
16
|
+
import { mod } from "./core.js";
|
|
17
|
+
import { DOMICILE, EXALTATION } from "./chart.js";
|
|
18
|
+
export const PLANETS = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn"];
|
|
19
|
+
export const DIGNITY_WEIGHTS = {
|
|
20
|
+
rulership: 5, exaltation: 4, triplicity: 3, term: 2, face: 1, detriment: -5, fall: -4,
|
|
21
|
+
};
|
|
22
|
+
/** Dorothean triplicity rulers by element (sign%4), as [day, night, participating]. */
|
|
23
|
+
export const TRIPLICITY = [
|
|
24
|
+
["sun", "jupiter", "saturn"], // fire
|
|
25
|
+
["venus", "moon", "mars"], // earth
|
|
26
|
+
["saturn", "mercury", "jupiter"], // air
|
|
27
|
+
["venus", "mars", "moon"], // water
|
|
28
|
+
];
|
|
29
|
+
/** Egyptian terms (Ptolemy I.21): per sign, [ruler, upper-degree] segments. */
|
|
30
|
+
export const TERMS_EGYPTIAN = [
|
|
31
|
+
[["jupiter", 6], ["venus", 12], ["mercury", 20], ["mars", 25], ["saturn", 30]], // Aries
|
|
32
|
+
[["venus", 8], ["mercury", 14], ["jupiter", 22], ["saturn", 27], ["mars", 30]], // Taurus
|
|
33
|
+
[["mercury", 6], ["jupiter", 12], ["venus", 17], ["mars", 24], ["saturn", 30]], // Gemini
|
|
34
|
+
[["mars", 7], ["venus", 13], ["mercury", 19], ["jupiter", 26], ["saturn", 30]], // Cancer
|
|
35
|
+
[["jupiter", 6], ["venus", 11], ["saturn", 18], ["mercury", 24], ["mars", 30]], // Leo
|
|
36
|
+
[["mercury", 7], ["venus", 17], ["jupiter", 21], ["mars", 28], ["saturn", 30]], // Virgo
|
|
37
|
+
[["saturn", 6], ["mercury", 14], ["jupiter", 21], ["venus", 28], ["mars", 30]], // Libra
|
|
38
|
+
[["mars", 7], ["venus", 11], ["mercury", 19], ["jupiter", 24], ["saturn", 30]], // Scorpio
|
|
39
|
+
[["jupiter", 12], ["venus", 17], ["mercury", 21], ["saturn", 26], ["mars", 30]], // Sagittarius
|
|
40
|
+
[["mercury", 7], ["jupiter", 14], ["venus", 22], ["saturn", 26], ["mars", 30]], // Capricorn
|
|
41
|
+
[["mercury", 7], ["venus", 13], ["jupiter", 20], ["mars", 25], ["saturn", 30]], // Aquarius
|
|
42
|
+
[["venus", 12], ["jupiter", 16], ["mercury", 19], ["mars", 28], ["saturn", 30]], // Pisces
|
|
43
|
+
];
|
|
44
|
+
/** Faces (decans): Chaldean order from Mars at 0 Aries; floor(lon/10) selects. */
|
|
45
|
+
export const FACE_CYCLE = ["mars", "sun", "venus", "mercury", "moon", "saturn", "jupiter"];
|
|
46
|
+
export function termRuler(sign, degInSign, terms = TERMS_EGYPTIAN) {
|
|
47
|
+
for (const [ruler, upper] of terms[sign])
|
|
48
|
+
if (degInSign < upper)
|
|
49
|
+
return ruler;
|
|
50
|
+
return terms[sign][terms[sign].length - 1][0];
|
|
51
|
+
}
|
|
52
|
+
export function faceRuler(lon) {
|
|
53
|
+
return FACE_CYCLE[Math.floor(mod(lon, 360) / 10) % 7];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The weighted essential dignities of `planet` at ecliptic longitude `lon`
|
|
57
|
+
* (degrees) in a day or night chart. Only the seven classical planets score.
|
|
58
|
+
*
|
|
59
|
+
* @param planet One of {@link PLANETS}.
|
|
60
|
+
* @param lon Ecliptic longitude in degrees.
|
|
61
|
+
* @param sect `"day"` or `"night"`, selecting the triplicity ruler.
|
|
62
|
+
* @param terms A term table; defaults to the Egyptian bounds.
|
|
63
|
+
* @returns A {@link DignityScore}.
|
|
64
|
+
*/
|
|
65
|
+
export function dignityScore(planet, lon, sect = "day", terms = TERMS_EGYPTIAN) {
|
|
66
|
+
const L = mod(lon, 360);
|
|
67
|
+
const sign = Math.floor(L / 30) % 12;
|
|
68
|
+
const deg = L - sign * 30;
|
|
69
|
+
const held = {};
|
|
70
|
+
if (DOMICILE[planet]?.includes(sign))
|
|
71
|
+
held.rulership = DIGNITY_WEIGHTS.rulership;
|
|
72
|
+
if (EXALTATION[planet] === sign)
|
|
73
|
+
held.exaltation = DIGNITY_WEIGHTS.exaltation;
|
|
74
|
+
const trip = TRIPLICITY[sign % 4][sect === "day" ? 0 : 1];
|
|
75
|
+
if (planet === trip)
|
|
76
|
+
held.triplicity = DIGNITY_WEIGHTS.triplicity;
|
|
77
|
+
const tr = termRuler(sign, deg, terms);
|
|
78
|
+
if (planet === tr)
|
|
79
|
+
held.term = DIGNITY_WEIGHTS.term;
|
|
80
|
+
const fr = faceRuler(L);
|
|
81
|
+
if (planet === fr)
|
|
82
|
+
held.face = DIGNITY_WEIGHTS.face;
|
|
83
|
+
if (DOMICILE[planet]?.some((d) => (d + 6) % 12 === sign))
|
|
84
|
+
held.detriment = DIGNITY_WEIGHTS.detriment;
|
|
85
|
+
if (planet in EXALTATION && (EXALTATION[planet] + 6) % 12 === sign)
|
|
86
|
+
held.fall = DIGNITY_WEIGHTS.fall;
|
|
87
|
+
const positive = ["rulership", "exaltation", "triplicity", "term", "face"].some((k) => k in held);
|
|
88
|
+
return {
|
|
89
|
+
planet,
|
|
90
|
+
rulership: held.rulership ?? 0,
|
|
91
|
+
exaltation: held.exaltation ?? 0,
|
|
92
|
+
triplicity: held.triplicity ?? 0,
|
|
93
|
+
term: held.term ?? 0,
|
|
94
|
+
face: held.face ?? 0,
|
|
95
|
+
detriment: held.detriment ?? 0,
|
|
96
|
+
fall: held.fall ?? 0,
|
|
97
|
+
total: Object.values(held).reduce((a, b) => a + b, 0),
|
|
98
|
+
peregrine: !positive,
|
|
99
|
+
term_ruler: tr,
|
|
100
|
+
face_ruler: fr,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The almuten of a degree: the classical planet with the greatest positive
|
|
105
|
+
* essential dignity (rulership + exaltation + triplicity + term + face) at `lon`.
|
|
106
|
+
* Ties broken by the canonical planet order.
|
|
107
|
+
*
|
|
108
|
+
* @returns `{ planet, score }`.
|
|
109
|
+
*/
|
|
110
|
+
export function almuten(lon, sect = "day", terms = TERMS_EGYPTIAN) {
|
|
111
|
+
let best = null;
|
|
112
|
+
let bestScore = -1;
|
|
113
|
+
for (const p of PLANETS) {
|
|
114
|
+
const d = dignityScore(p, lon, sect, terms);
|
|
115
|
+
const score = d.rulership + d.exaltation + d.triplicity + d.term + d.face;
|
|
116
|
+
if (score > bestScore) {
|
|
117
|
+
bestScore = score;
|
|
118
|
+
best = p;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { planet: best, score: bestScore };
|
|
122
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -26,3 +26,7 @@ export * from "./yogini.js";
|
|
|
26
26
|
export * from "./yogas.js";
|
|
27
27
|
export * from "./ashtottari.js";
|
|
28
28
|
export * from "./rajayoga.js";
|
|
29
|
+
export * from "./patterns.js";
|
|
30
|
+
export * from "./signature.js";
|
|
31
|
+
export * from "./dignity-score.js";
|
|
32
|
+
export * from "./parans.js";
|
package/dist/src/index.js
CHANGED
|
@@ -26,3 +26,7 @@ export * from "./yogini.js";
|
|
|
26
26
|
export * from "./yogas.js";
|
|
27
27
|
export * from "./ashtottari.js";
|
|
28
28
|
export * from "./rajayoga.js";
|
|
29
|
+
export * from "./patterns.js";
|
|
30
|
+
export * from "./signature.js";
|
|
31
|
+
export * from "./dignity-score.js";
|
|
32
|
+
export * from "./parans.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Engine } from "./chart.js";
|
|
2
|
+
export declare const PARAN_ANGLES: readonly ["rise", "mtransit", "set", "itransit"];
|
|
3
|
+
export declare const DEFAULT_PARAN_BODIES: string[];
|
|
4
|
+
/** One co-angular pair: bodies `a` and `b` on the named angles at ~the same time. */
|
|
5
|
+
export interface Paran {
|
|
6
|
+
a: string;
|
|
7
|
+
a_angle: string;
|
|
8
|
+
b: string;
|
|
9
|
+
b_angle: string;
|
|
10
|
+
/** Midpoint instant of the two angle crossings, Julian Day (UT). */
|
|
11
|
+
jd: number;
|
|
12
|
+
/** Gap between the two crossings, minutes. */
|
|
13
|
+
gap_min: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Co-angular pairs over the 24 hours from `jd` (UT) at latitude `lat`: every
|
|
17
|
+
* pair of different bodies whose angle crossings fall within `toleranceMin`
|
|
18
|
+
* minutes. Ordered by (a, b, jd), with `a` < `b` by name.
|
|
19
|
+
*
|
|
20
|
+
* @param engine The engine used to evaluate rise/set/transit times.
|
|
21
|
+
* @param jd Julian Day in UT (the 24-hour window starts here).
|
|
22
|
+
* @param lat Geographic latitude in degrees, north positive.
|
|
23
|
+
* @param bodies Bodies to consider; defaults to the seven classical planets.
|
|
24
|
+
* @param toleranceMin The paran window in minutes (default 30).
|
|
25
|
+
* @returns The co-angular pairs as {@link Paran} objects.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parans(engine: Engine, jd: number, lat: number, bodies?: string[], toleranceMin?: number): Paran[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paranatellonta (parans): co-angular bodies.
|
|
3
|
+
*
|
|
4
|
+
* Two bodies are in paran on a given day at a given latitude when both are
|
|
5
|
+
* simultaneously on one of the four angles: rising, culminating (upper
|
|
6
|
+
* meridian), setting, or anti-culminating (lower meridian) — the relationship
|
|
7
|
+
* behind the fixed-star parans of Brady's tradition, computed here for the
|
|
8
|
+
* moving bodies.
|
|
9
|
+
*
|
|
10
|
+
* Pure positional astronomy over the validated rise/set/transit times, with a
|
|
11
|
+
* stated tolerance (not a hidden convention). Longitude-independent, so latitude
|
|
12
|
+
* alone is needed. Port of the Python reference `astroengine.parans`, pinned by
|
|
13
|
+
* `parans-golden`.
|
|
14
|
+
*/
|
|
15
|
+
import { riseSet } from "./events.js";
|
|
16
|
+
export const PARAN_ANGLES = ["rise", "mtransit", "set", "itransit"];
|
|
17
|
+
export const DEFAULT_PARAN_BODIES = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn"];
|
|
18
|
+
/**
|
|
19
|
+
* Co-angular pairs over the 24 hours from `jd` (UT) at latitude `lat`: every
|
|
20
|
+
* pair of different bodies whose angle crossings fall within `toleranceMin`
|
|
21
|
+
* minutes. Ordered by (a, b, jd), with `a` < `b` by name.
|
|
22
|
+
*
|
|
23
|
+
* @param engine The engine used to evaluate rise/set/transit times.
|
|
24
|
+
* @param jd Julian Day in UT (the 24-hour window starts here).
|
|
25
|
+
* @param lat Geographic latitude in degrees, north positive.
|
|
26
|
+
* @param bodies Bodies to consider; defaults to the seven classical planets.
|
|
27
|
+
* @param toleranceMin The paran window in minutes (default 30).
|
|
28
|
+
* @returns The co-angular pairs as {@link Paran} objects.
|
|
29
|
+
*/
|
|
30
|
+
export function parans(engine, jd, lat, bodies = DEFAULT_PARAN_BODIES, toleranceMin = 30) {
|
|
31
|
+
const events = [];
|
|
32
|
+
for (const b of bodies) {
|
|
33
|
+
for (const kind of PARAN_ANGLES) {
|
|
34
|
+
const t = riseSet(engine, b, jd, lat, 0, kind);
|
|
35
|
+
if (t !== null && t < jd + 1)
|
|
36
|
+
events.push([b, kind, t]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let i = 0; i < events.length; i++) {
|
|
41
|
+
for (let j = i + 1; j < events.length; j++) {
|
|
42
|
+
const [ab, aa, ta] = events[i];
|
|
43
|
+
const [bb, ba, tb] = events[j];
|
|
44
|
+
if (ab === bb)
|
|
45
|
+
continue;
|
|
46
|
+
const gap = Math.abs(ta - tb) * 1440;
|
|
47
|
+
if (gap > toleranceMin)
|
|
48
|
+
continue;
|
|
49
|
+
const [pa, paa, pb, pba] = ab <= bb ? [ab, aa, bb, ba] : [bb, ba, ab, aa];
|
|
50
|
+
out.push({
|
|
51
|
+
a: pa, a_angle: paa, b: pb, b_angle: pba,
|
|
52
|
+
jd: Math.round(((ta + tb) / 2) * 1e6) / 1e6,
|
|
53
|
+
gap_min: Math.round(gap * 1e4) / 1e4,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
out.sort((x, y) => (x.a < y.a ? -1 : x.a > y.a ? 1 : x.b < y.b ? -1 : x.b > y.b ? 1 : x.jd - y.jd));
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Chart } from "./chart.js";
|
|
2
|
+
/** Pattern aspects, including the quincunx the default Ptolemaic set omits. */
|
|
3
|
+
export declare const PATTERN_ANGLES: Record<string, number>;
|
|
4
|
+
/** Default orbs: {@link DEFAULT_ORBS} for the five Ptolemaic aspects, plus a
|
|
5
|
+
* tight quincunx. Override via {@link PatternOptions.orbs}. */
|
|
6
|
+
export declare const PATTERN_ORBS: Record<string, number>;
|
|
7
|
+
/** One configuration found in a chart. */
|
|
8
|
+
export interface ChartPattern {
|
|
9
|
+
/** Configuration kind, e.g. `"t_square"` or `"grand_trine"`. */
|
|
10
|
+
kind: string;
|
|
11
|
+
/** Participating body ids, sorted. */
|
|
12
|
+
bodies: string[];
|
|
13
|
+
/** Focal body for a T-square or yod (the squared / quincunx apex). */
|
|
14
|
+
apex?: string;
|
|
15
|
+
/** Sign for a `stellium_sign`. */
|
|
16
|
+
sign?: string;
|
|
17
|
+
/** House for a `stellium_house`. */
|
|
18
|
+
house?: number;
|
|
19
|
+
/** Worst defining-aspect orb in degrees; `0` for stelliums. */
|
|
20
|
+
orb: number;
|
|
21
|
+
}
|
|
22
|
+
/** A body's longitude (and house, for stelliums) for {@link detectPatternsIn}. */
|
|
23
|
+
export interface PatternBody {
|
|
24
|
+
lon: number;
|
|
25
|
+
house?: number | null;
|
|
26
|
+
}
|
|
27
|
+
export interface PatternOptions {
|
|
28
|
+
/** Per-aspect orb overrides (degrees), keyed by aspect name. */
|
|
29
|
+
orbs?: Record<string, number>;
|
|
30
|
+
/** Body ids to consider; defaults to the aspectable bodies present. */
|
|
31
|
+
bodies?: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The configurations present among a body map. Lower-level form of
|
|
35
|
+
* {@link detectPatterns}: `bodies` maps a body id to `{ lon, house? }`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function detectPatternsIn(bodies: Record<string, PatternBody>, opts?: PatternOptions): ChartPattern[];
|
|
38
|
+
/**
|
|
39
|
+
* The aspect configurations present in a {@link Chart}: T-squares, grand trines,
|
|
40
|
+
* grand crosses, yods, kites, mystic rectangles, and stelliums by sign and by
|
|
41
|
+
* house. Bodies outside their fitted range (absent from the chart) are skipped.
|
|
42
|
+
*
|
|
43
|
+
* @param chart A {@link Chart} from {@link Engine.chart} / {@link Engine.chartAt}.
|
|
44
|
+
* @param opts Orb overrides and an optional explicit body set.
|
|
45
|
+
* @returns The configurations, most-complex first, each a {@link ChartPattern}.
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const chart = engine.chart(1990, 6, 10, 14, 30, 0, 27.95, -82.46, "placidus");
|
|
49
|
+
* detectPatterns(chart); // [{ kind: "mystic_rectangle", bodies: [...], orb: 2.54 }, ...]
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function detectPatterns(chart: Chart, opts?: PatternOptions): ChartPattern[];
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classical aspect configurations as first-class objects.
|
|
3
|
+
*
|
|
4
|
+
* Pure geometry over a chart's body longitudes (and houses, for stelliums); no
|
|
5
|
+
* interpretation. Each configuration is judged from pairwise angular separations
|
|
6
|
+
* against an explicit, overridable orb policy: the engine's {@link DEFAULT_ORBS}
|
|
7
|
+
* for the Ptolemaic aspects, plus a quincunx (150°) for yods. The default body
|
|
8
|
+
* set is the aspectable bodies (planets and Chiron; nodes and Lilith excluded).
|
|
9
|
+
*
|
|
10
|
+
* Reported patterns are maximal: a grand cross suppresses the two T-squares it
|
|
11
|
+
* contains and a kite suppresses its grand trine. Port of the Python reference
|
|
12
|
+
* `astroengine.patterns`, pinned by `patterns-golden`.
|
|
13
|
+
*/
|
|
14
|
+
import { mod } from "./core.js";
|
|
15
|
+
import { SIGNS, NOT_ASPECTABLE } from "./chart.js";
|
|
16
|
+
/** Pattern aspects, including the quincunx the default Ptolemaic set omits. */
|
|
17
|
+
export const PATTERN_ANGLES = {
|
|
18
|
+
conjunction: 0, sextile: 60, square: 90, trine: 120, quincunx: 150, opposition: 180,
|
|
19
|
+
};
|
|
20
|
+
/** Default orbs: {@link DEFAULT_ORBS} for the five Ptolemaic aspects, plus a
|
|
21
|
+
* tight quincunx. Override via {@link PatternOptions.orbs}. */
|
|
22
|
+
export const PATTERN_ORBS = {
|
|
23
|
+
conjunction: 8, sextile: 4, square: 7, trine: 7, quincunx: 3, opposition: 8,
|
|
24
|
+
};
|
|
25
|
+
const KIND_ORDER = [
|
|
26
|
+
"grand_cross", "mystic_rectangle", "kite", "t_square", "grand_trine",
|
|
27
|
+
"yod", "stellium_sign", "stellium_house",
|
|
28
|
+
];
|
|
29
|
+
const separation = (la, lb) => Math.abs(mod(la - lb + 180, 360) - 180);
|
|
30
|
+
/** The single aspect a pair forms (orbs do not overlap), as `[name, orb]`, or null. */
|
|
31
|
+
function relation(la, lb, orbs) {
|
|
32
|
+
const sep = separation(la, lb);
|
|
33
|
+
for (const name of Object.keys(PATTERN_ANGLES)) {
|
|
34
|
+
const orb = Math.abs(sep - PATTERN_ANGLES[name]);
|
|
35
|
+
if (orb <= orbs[name])
|
|
36
|
+
return [name, orb];
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function cmpBodies(a, b) {
|
|
41
|
+
const n = Math.min(a.length, b.length);
|
|
42
|
+
for (let i = 0; i < n; i++)
|
|
43
|
+
if (a[i] !== b[i])
|
|
44
|
+
return a[i] < b[i] ? -1 : 1;
|
|
45
|
+
return a.length - b.length;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The configurations present among a body map. Lower-level form of
|
|
49
|
+
* {@link detectPatterns}: `bodies` maps a body id to `{ lon, house? }`.
|
|
50
|
+
*/
|
|
51
|
+
export function detectPatternsIn(bodies, opts = {}) {
|
|
52
|
+
const orbs = opts.orbs ?? PATTERN_ORBS;
|
|
53
|
+
const names = (opts.bodies ?? Object.keys(bodies).filter((b) => !NOT_ASPECTABLE.has(b)))
|
|
54
|
+
.filter((b) => b in bodies);
|
|
55
|
+
const lon = {};
|
|
56
|
+
for (const b of names)
|
|
57
|
+
lon[b] = mod(bodies[b].lon, 360);
|
|
58
|
+
const key = (a, b) => (a < b ? `${a}|${b}` : `${b}|${a}`);
|
|
59
|
+
const rel = new Map();
|
|
60
|
+
for (let i = 0; i < names.length; i++) {
|
|
61
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
62
|
+
const r = relation(lon[names[i]], lon[names[j]], orbs);
|
|
63
|
+
if (r)
|
|
64
|
+
rel.set(key(names[i], names[j]), r);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const asp = (a, b) => rel.get(key(a, b)) ?? null;
|
|
68
|
+
const isAspect = (a, b, kind) => {
|
|
69
|
+
const r = asp(a, b);
|
|
70
|
+
return r !== null && r[0] === kind;
|
|
71
|
+
};
|
|
72
|
+
const out = [];
|
|
73
|
+
// Grand trines (3-body) and grand crosses / mystic rectangles (4-body).
|
|
74
|
+
const grandTrines = [];
|
|
75
|
+
for (let i = 0; i < names.length; i++) {
|
|
76
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
77
|
+
for (let k = j + 1; k < names.length; k++) {
|
|
78
|
+
const [a, b, c] = [names[i], names[j], names[k]];
|
|
79
|
+
if (isAspect(a, b, "trine") && isAspect(b, c, "trine") && isAspect(a, c, "trine")) {
|
|
80
|
+
const orb = Math.max(asp(a, b)[1], asp(b, c)[1], asp(a, c)[1]);
|
|
81
|
+
grandTrines.push({ kind: "grand_trine", bodies: [a, b, c].sort(), orb });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const grandCrosses = [];
|
|
87
|
+
const mysticRectangles = [];
|
|
88
|
+
for (let i = 0; i < names.length; i++) {
|
|
89
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
90
|
+
for (let k = j + 1; k < names.length; k++) {
|
|
91
|
+
for (let l = k + 1; l < names.length; l++) {
|
|
92
|
+
const quad = [names[i], names[j], names[k], names[l]];
|
|
93
|
+
const pairs = [
|
|
94
|
+
[quad[0], quad[1]], [quad[0], quad[2]], [quad[0], quad[3]],
|
|
95
|
+
[quad[1], quad[2]], [quad[1], quad[3]], [quad[2], quad[3]],
|
|
96
|
+
];
|
|
97
|
+
const kinds = pairs.map(([a, b]) => asp(a, b));
|
|
98
|
+
if (kinds.some((r) => r === null))
|
|
99
|
+
continue;
|
|
100
|
+
const counts = {};
|
|
101
|
+
let worst = 0;
|
|
102
|
+
for (const r of kinds) {
|
|
103
|
+
counts[r[0]] = (counts[r[0]] ?? 0) + 1;
|
|
104
|
+
if (r[1] > worst)
|
|
105
|
+
worst = r[1];
|
|
106
|
+
}
|
|
107
|
+
if (counts.opposition === 2 && counts.square === 4) {
|
|
108
|
+
grandCrosses.push({ kind: "grand_cross", bodies: [...quad].sort(), orb: worst });
|
|
109
|
+
}
|
|
110
|
+
else if (counts.opposition === 2 && counts.trine === 2 && counts.sextile === 2) {
|
|
111
|
+
mysticRectangles.push({ kind: "mystic_rectangle", bodies: [...quad].sort(), orb: worst });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Kite: a grand trine plus a fourth body opposite one member.
|
|
118
|
+
const kites = [];
|
|
119
|
+
for (const gt of grandTrines) {
|
|
120
|
+
const tri = gt.bodies;
|
|
121
|
+
for (const d of names) {
|
|
122
|
+
if (tri.includes(d))
|
|
123
|
+
continue;
|
|
124
|
+
for (const apex of tri) {
|
|
125
|
+
const others = tri.filter((x) => x !== apex);
|
|
126
|
+
if (isAspect(d, apex, "opposition")
|
|
127
|
+
&& isAspect(d, others[0], "sextile")
|
|
128
|
+
&& isAspect(d, others[1], "sextile")) {
|
|
129
|
+
const orb = Math.max(gt.orb, asp(d, apex)[1], asp(d, others[0])[1], asp(d, others[1])[1]);
|
|
130
|
+
kites.push({ kind: "kite", bodies: [...tri, d].sort(), apex, orb });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// T-square: an opposition whose two ends both square a common apex.
|
|
136
|
+
const tSquares = [];
|
|
137
|
+
for (let i = 0; i < names.length; i++) {
|
|
138
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
139
|
+
const [a, b] = [names[i], names[j]];
|
|
140
|
+
if (!isAspect(a, b, "opposition"))
|
|
141
|
+
continue;
|
|
142
|
+
for (const apex of names) {
|
|
143
|
+
if (apex === a || apex === b)
|
|
144
|
+
continue;
|
|
145
|
+
if (isAspect(apex, a, "square") && isAspect(apex, b, "square")) {
|
|
146
|
+
const orb = Math.max(asp(a, b)[1], asp(apex, a)[1], asp(apex, b)[1]);
|
|
147
|
+
tSquares.push({ kind: "t_square", bodies: [a, b, apex].sort(), apex, orb });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Yod: a sextile whose two ends both quincunx a common apex.
|
|
153
|
+
const yods = [];
|
|
154
|
+
for (let i = 0; i < names.length; i++) {
|
|
155
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
156
|
+
const [a, b] = [names[i], names[j]];
|
|
157
|
+
if (!isAspect(a, b, "sextile"))
|
|
158
|
+
continue;
|
|
159
|
+
for (const apex of names) {
|
|
160
|
+
if (apex === a || apex === b)
|
|
161
|
+
continue;
|
|
162
|
+
if (isAspect(apex, a, "quincunx") && isAspect(apex, b, "quincunx")) {
|
|
163
|
+
const orb = Math.max(asp(a, b)[1], asp(apex, a)[1], asp(apex, b)[1]);
|
|
164
|
+
yods.push({ kind: "yod", bodies: [a, b, apex].sort(), apex, orb });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Suppress sub-patterns contained in a larger reported one.
|
|
170
|
+
const subset = (small, big) => small.every((x) => big.includes(x));
|
|
171
|
+
const keptTSquares = tSquares.filter((t) => !grandCrosses.some((g) => subset(t.bodies, g.bodies)));
|
|
172
|
+
const keptTrines = grandTrines.filter((g) => !kites.some((k) => subset(g.bodies, k.bodies)));
|
|
173
|
+
out.push(...grandCrosses, ...mysticRectangles, ...kites, ...keptTSquares, ...keptTrines, ...yods);
|
|
174
|
+
// Stelliums by sign and by house: three or more bodies sharing one.
|
|
175
|
+
const bySign = {};
|
|
176
|
+
for (const b of names) {
|
|
177
|
+
const s = Math.floor(lon[b] / 30) % 12;
|
|
178
|
+
(bySign[s] ??= []).push(b);
|
|
179
|
+
}
|
|
180
|
+
for (const s of Object.keys(bySign)) {
|
|
181
|
+
const members = bySign[Number(s)];
|
|
182
|
+
if (members.length >= 3) {
|
|
183
|
+
out.push({ kind: "stellium_sign", bodies: [...members].sort(), sign: SIGNS[Number(s)], orb: 0 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const byHouse = {};
|
|
187
|
+
for (const b of names) {
|
|
188
|
+
const h = bodies[b].house;
|
|
189
|
+
if (h != null)
|
|
190
|
+
(byHouse[h] ??= []).push(b);
|
|
191
|
+
}
|
|
192
|
+
for (const h of Object.keys(byHouse)) {
|
|
193
|
+
const members = byHouse[Number(h)];
|
|
194
|
+
if (members.length >= 3) {
|
|
195
|
+
out.push({ kind: "stellium_house", bodies: [...members].sort(), house: Number(h), orb: 0 });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
out.sort((a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) || cmpBodies(a.bodies, b.bodies));
|
|
199
|
+
for (const p of out)
|
|
200
|
+
p.orb = Math.round(p.orb * 1e4) / 1e4;
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The aspect configurations present in a {@link Chart}: T-squares, grand trines,
|
|
205
|
+
* grand crosses, yods, kites, mystic rectangles, and stelliums by sign and by
|
|
206
|
+
* house. Bodies outside their fitted range (absent from the chart) are skipped.
|
|
207
|
+
*
|
|
208
|
+
* @param chart A {@link Chart} from {@link Engine.chart} / {@link Engine.chartAt}.
|
|
209
|
+
* @param opts Orb overrides and an optional explicit body set.
|
|
210
|
+
* @returns The configurations, most-complex first, each a {@link ChartPattern}.
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* const chart = engine.chart(1990, 6, 10, 14, 30, 0, 27.95, -82.46, "placidus");
|
|
214
|
+
* detectPatterns(chart); // [{ kind: "mystic_rectangle", bodies: [...], orb: 2.54 }, ...]
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
export function detectPatterns(chart, opts = {}) {
|
|
218
|
+
const bodies = {};
|
|
219
|
+
for (const [name, p] of Object.entries(chart.bodies)) {
|
|
220
|
+
if (p)
|
|
221
|
+
bodies[name] = { lon: p.lon, house: p.house };
|
|
222
|
+
}
|
|
223
|
+
return detectPatternsIn(bodies, opts);
|
|
224
|
+
}
|
package/dist/src/rajayoga.js
CHANGED
|
@@ -9,6 +9,7 @@ export const TRIKONAS = [1, 5, 9];
|
|
|
9
9
|
export const DHANA_HOUSES = [2, 5, 9, 11];
|
|
10
10
|
const PURE_KENDRAS = [4, 7, 10];
|
|
11
11
|
const PURE_TRIKONAS = [5, 9];
|
|
12
|
+
// The seven classical grahas: all analytic, so always present in a chart.
|
|
12
13
|
const PLANETS = ["sun", "moon", "mars", "mercury", "jupiter", "venus", "saturn"];
|
|
13
14
|
/** The traditional ruler of a sign index (0 = Aries). */
|
|
14
15
|
export function signLord(sign) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Chart } from "./chart.js";
|
|
2
|
+
export declare const ELEMENTS: readonly ["fire", "earth", "air", "water"];
|
|
3
|
+
export declare const MODALITIES: readonly ["cardinal", "fixed", "mutable"];
|
|
4
|
+
export interface ChartSignature {
|
|
5
|
+
/** Bodies per element (fire/earth/air/water). */
|
|
6
|
+
elements: Record<string, number>;
|
|
7
|
+
/** Bodies per modality (cardinal/fixed/mutable). */
|
|
8
|
+
modalities: Record<string, number>;
|
|
9
|
+
/** Bodies per angularity (angular/succedent/cadent), house-based. */
|
|
10
|
+
angularity: Record<string, number>;
|
|
11
|
+
/** Bodies per quadrant ("1"-"4"), house-based. */
|
|
12
|
+
quadrants: Record<string, number>;
|
|
13
|
+
/** Bodies above/below the horizon and east/west, house-based. */
|
|
14
|
+
hemispheres: Record<string, number>;
|
|
15
|
+
/** Argmax of the distributions; `sign` is the most-occupied sign (>=2) or null. */
|
|
16
|
+
dominant: {
|
|
17
|
+
element: string;
|
|
18
|
+
modality: string;
|
|
19
|
+
sign: string | null;
|
|
20
|
+
};
|
|
21
|
+
/** Classical ruler of the Ascendant's sign, or null when no Ascendant given. */
|
|
22
|
+
ruler: string | null;
|
|
23
|
+
/** The bodies counted, sorted. */
|
|
24
|
+
bodies: string[];
|
|
25
|
+
}
|
|
26
|
+
/** A body's longitude (and house) for {@link chartSignatureOf}. */
|
|
27
|
+
export interface SignatureBody {
|
|
28
|
+
lon: number;
|
|
29
|
+
house?: number | null;
|
|
30
|
+
}
|
|
31
|
+
export interface SignatureOptions {
|
|
32
|
+
/** Ascendant sign index 0-11; yields the classical chart ruler. */
|
|
33
|
+
ascSign?: number | null;
|
|
34
|
+
/** Body ids to count; defaults to the aspectable bodies present. */
|
|
35
|
+
bodies?: string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Structural counts for a body map. Lower-level form of {@link chartSignature}:
|
|
39
|
+
* `bodies` maps a body id to `{ lon, house? }`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function chartSignatureOf(bodies: Record<string, SignatureBody>, opts?: SignatureOptions): ChartSignature;
|
|
42
|
+
/**
|
|
43
|
+
* The structural signature of a {@link Chart}: element / modality / angularity /
|
|
44
|
+
* quadrant / hemisphere distributions over its bodies, the dominant element,
|
|
45
|
+
* modality, and most-occupied sign, and the classical chart ruler. Counts only,
|
|
46
|
+
* no interpretation. Bodies absent from the chart (outside their fitted range)
|
|
47
|
+
* are skipped.
|
|
48
|
+
*
|
|
49
|
+
* @param chart A {@link Chart} from {@link Engine.chart} / {@link Engine.chartAt}.
|
|
50
|
+
* @param opts An explicit body set, or an Ascendant-sign override.
|
|
51
|
+
* @returns A {@link ChartSignature}.
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const chart = engine.chart(1990, 6, 10, 14, 30, 0, 27.95, -82.46, "placidus");
|
|
55
|
+
* chartSignature(chart).dominant; // { element: "earth", modality: "cardinal", sign: "Capricorn" }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function chartSignature(chart: Chart, opts?: SignatureOptions): ChartSignature;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chart's structural signature, as plain counts.
|
|
3
|
+
*
|
|
4
|
+
* Element / modality distributions (from each body's sign), angularity /
|
|
5
|
+
* quadrant / hemisphere distributions (from its house), the dominant element,
|
|
6
|
+
* modality, and most-occupied sign (argmax of the counts), and the classical
|
|
7
|
+
* chart ruler (the domicile ruler of the Ascendant's sign). No interpretation,
|
|
8
|
+
* no "flavour" labels.
|
|
9
|
+
*
|
|
10
|
+
* The only convention is which bodies are counted and that each counts once: the
|
|
11
|
+
* default is the aspectable bodies (planets and Chiron; nodes and Lilith
|
|
12
|
+
* excluded), each weight 1. Weighted "dominance" schemes are deliberately not the
|
|
13
|
+
* default. Port of the Python reference `astroengine.signature`, pinned by
|
|
14
|
+
* `signature-golden`.
|
|
15
|
+
*/
|
|
16
|
+
import { mod } from "./core.js";
|
|
17
|
+
import { SIGNS, NOT_ASPECTABLE } from "./chart.js";
|
|
18
|
+
export const ELEMENTS = ["fire", "earth", "air", "water"];
|
|
19
|
+
export const MODALITIES = ["cardinal", "fixed", "mutable"];
|
|
20
|
+
const ANGULARITY = ["angular", "succedent", "cadent"];
|
|
21
|
+
/** Classical (domicile) ruler by sign index 0-11, matching the engine's dignities. */
|
|
22
|
+
const RULERS = ["mars", "venus", "mercury", "moon", "sun", "mercury",
|
|
23
|
+
"venus", "mars", "jupiter", "saturn", "saturn", "jupiter"];
|
|
24
|
+
function argmax(counts, order) {
|
|
25
|
+
let best = order[0];
|
|
26
|
+
let bestV = -1;
|
|
27
|
+
for (const k of order)
|
|
28
|
+
if (counts[k] > bestV) {
|
|
29
|
+
bestV = counts[k];
|
|
30
|
+
best = k;
|
|
31
|
+
}
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Structural counts for a body map. Lower-level form of {@link chartSignature}:
|
|
36
|
+
* `bodies` maps a body id to `{ lon, house? }`.
|
|
37
|
+
*/
|
|
38
|
+
export function chartSignatureOf(bodies, opts = {}) {
|
|
39
|
+
const names = (opts.bodies ?? Object.keys(bodies).filter((b) => !NOT_ASPECTABLE.has(b)))
|
|
40
|
+
.filter((b) => b in bodies);
|
|
41
|
+
const elements = { fire: 0, earth: 0, air: 0, water: 0 };
|
|
42
|
+
const modalities = { cardinal: 0, fixed: 0, mutable: 0 };
|
|
43
|
+
const angularity = { angular: 0, succedent: 0, cadent: 0 };
|
|
44
|
+
const quadrants = { 1: 0, 2: 0, 3: 0, 4: 0 };
|
|
45
|
+
const hemispheres = { above: 0, below: 0, eastern: 0, western: 0 };
|
|
46
|
+
const signCounts = {};
|
|
47
|
+
for (const b of names) {
|
|
48
|
+
const sign = Math.floor(mod(bodies[b].lon, 360) / 30) % 12;
|
|
49
|
+
elements[ELEMENTS[sign % 4]]++;
|
|
50
|
+
modalities[MODALITIES[sign % 3]]++;
|
|
51
|
+
signCounts[sign] = (signCounts[sign] ?? 0) + 1;
|
|
52
|
+
const h = bodies[b].house;
|
|
53
|
+
if (h != null) {
|
|
54
|
+
angularity[ANGULARITY[(h - 1) % 3]]++;
|
|
55
|
+
quadrants[String(Math.floor((h - 1) / 3) + 1)]++;
|
|
56
|
+
hemispheres[h >= 7 ? "above" : "below"]++;
|
|
57
|
+
hemispheres[[10, 11, 12, 1, 2, 3].includes(h) ? "eastern" : "western"]++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let domSign = null;
|
|
61
|
+
let best = 1;
|
|
62
|
+
for (const s of Object.keys(signCounts).map(Number).sort((a, b) => a - b)) {
|
|
63
|
+
if (signCounts[s] > best) {
|
|
64
|
+
best = signCounts[s];
|
|
65
|
+
domSign = s;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
elements,
|
|
70
|
+
modalities,
|
|
71
|
+
angularity,
|
|
72
|
+
quadrants,
|
|
73
|
+
hemispheres,
|
|
74
|
+
dominant: {
|
|
75
|
+
element: argmax(elements, ELEMENTS),
|
|
76
|
+
modality: argmax(modalities, MODALITIES),
|
|
77
|
+
sign: domSign !== null ? SIGNS[domSign] : null,
|
|
78
|
+
},
|
|
79
|
+
ruler: opts.ascSign != null ? RULERS[opts.ascSign] : null,
|
|
80
|
+
bodies: [...names].sort(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The structural signature of a {@link Chart}: element / modality / angularity /
|
|
85
|
+
* quadrant / hemisphere distributions over its bodies, the dominant element,
|
|
86
|
+
* modality, and most-occupied sign, and the classical chart ruler. Counts only,
|
|
87
|
+
* no interpretation. Bodies absent from the chart (outside their fitted range)
|
|
88
|
+
* are skipped.
|
|
89
|
+
*
|
|
90
|
+
* @param chart A {@link Chart} from {@link Engine.chart} / {@link Engine.chartAt}.
|
|
91
|
+
* @param opts An explicit body set, or an Ascendant-sign override.
|
|
92
|
+
* @returns A {@link ChartSignature}.
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* const chart = engine.chart(1990, 6, 10, 14, 30, 0, 27.95, -82.46, "placidus");
|
|
96
|
+
* chartSignature(chart).dominant; // { element: "earth", modality: "cardinal", sign: "Capricorn" }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export function chartSignature(chart, opts = {}) {
|
|
100
|
+
const bodies = {};
|
|
101
|
+
for (const [name, p] of Object.entries(chart.bodies))
|
|
102
|
+
if (p)
|
|
103
|
+
bodies[name] = { lon: p.lon, house: p.house };
|
|
104
|
+
const ascSign = opts.ascSign ?? Math.floor(mod(chart.angles.asc, 360) / 30) % 12;
|
|
105
|
+
return chartSignatureOf(bodies, { ...opts, ascSign });
|
|
106
|
+
}
|
package/dist/src/yogas.d.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* variant-laden yogas (Kemadruma, lordship-based raja/dhana) are left to a later
|
|
13
13
|
* step. Mirrors the Python reference (astroengine/yogas.py).
|
|
14
14
|
*/
|
|
15
|
-
import { Engine,
|
|
16
|
-
export declare const YOGA_PLANETS:
|
|
15
|
+
import { Engine, Zodiac, AlwaysBody } from "./chart.js";
|
|
16
|
+
export declare const YOGA_PLANETS: readonly AlwaysBody[];
|
|
17
17
|
export interface Yoga {
|
|
18
18
|
yoga: string;
|
|
19
19
|
planets: string[];
|
package/dist/src/yogas.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
|
|
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 { dignities } from "./chart.js";
|
|
2
16
|
/** Pancha Mahapurusha: [yoga name, planet]. */
|
|
3
17
|
const MAHAPURUSHA = [
|
|
4
18
|
["Ruchaka", "mars"], ["Bhadra", "mercury"], ["Hamsa", "jupiter"],
|
|
5
19
|
["Malavya", "venus"], ["Shasha", "saturn"],
|
|
6
20
|
];
|
|
7
21
|
const KENDRA = new Set([1, 4, 7, 10]);
|
|
22
|
+
// The seven classical grahas: all analytic, so always present in a chart.
|
|
8
23
|
export const YOGA_PLANETS = ["sun", "moon", "mars", "mercury", "jupiter", "venus", "saturn"];
|
|
9
24
|
/** The placement yogas present in a chart. `signs` maps each of the seven
|
|
10
25
|
* classical planets to its 0-based sign index; `ascSign` is the Ascendant's
|