caelus 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ export type Constraint = {
2
+ kind: "aspect";
3
+ a: string;
4
+ b: string;
5
+ angle: number;
6
+ weight?: number;
7
+ } | {
8
+ kind: "sign";
9
+ body: string;
10
+ sign: number;
11
+ weight?: number;
12
+ } | {
13
+ kind: "degree";
14
+ body: string;
15
+ degree: number;
16
+ weight?: number;
17
+ };
18
+ /** Degrees by which a single constraint is unmet given the longitudes. */
19
+ export declare function constraintLoss(lons: Record<string, number>, c: Constraint): number;
20
+ /** Total weighted constraint loss for a set of body longitudes. */
21
+ export declare function formLoss(lons: Record<string, number>, constraints: Constraint[]): number;
22
+ export interface CompiledForm {
23
+ longitudes: Record<string, number>;
24
+ residual: number;
25
+ maxConstraintLoss: number;
26
+ impossible: boolean;
27
+ constraints: Array<Constraint & {
28
+ loss: number;
29
+ }>;
30
+ }
31
+ export interface CompileOptions {
32
+ restarts?: number;
33
+ iters?: number;
34
+ /** A form is impossible when its worst constraint exceeds this (degrees). */
35
+ impossibleDeg?: number;
36
+ }
37
+ /** Find body longitudes minimizing the weighted constraint loss. */
38
+ export declare function compileForm(constraints: Constraint[], opts?: CompileOptions): CompiledForm;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * astroengine compiler -- synthesize a chart form from geometric constraints.
3
+ *
4
+ * The inverse of (time, place) -> chart: given weighted geometric constraints
5
+ * (aspects between bodies, sign or degree placements), find the body longitudes
6
+ * that best satisfy them, and report how well they can be. If the best fit is
7
+ * still poor, the form is geometrically impossible -- a valid result.
8
+ *
9
+ * The loss / constraint math is pure and mirrors the Python reference
10
+ * (astroengine/compiler.py), pinned by the golden. The optimizer is a
11
+ * deterministic coordinate descent with fixed low-discrepancy restarts.
12
+ */
13
+ const PHI = 0.6180339887498949;
14
+ function angDist(a, b) {
15
+ return Math.abs(((a - b + 180.0) % 360.0) - 180.0);
16
+ }
17
+ function signLoss(lon, sign) {
18
+ const lo = (((sign % 12) + 12) % 12) * 30.0;
19
+ const d = (((lon - lo) % 360.0) + 360.0) % 360.0;
20
+ if (d < 30.0)
21
+ return 0.0;
22
+ return Math.min(d - 30.0, 360.0 - d);
23
+ }
24
+ /** Degrees by which a single constraint is unmet given the longitudes. */
25
+ export function constraintLoss(lons, c) {
26
+ if (c.kind === "aspect")
27
+ return Math.abs(angDist(lons[c.a], lons[c.b]) - c.angle);
28
+ if (c.kind === "sign")
29
+ return signLoss(lons[c.body], c.sign);
30
+ return angDist(lons[c.body], c.degree);
31
+ }
32
+ /** Total weighted constraint loss for a set of body longitudes. */
33
+ export function formLoss(lons, constraints) {
34
+ let total = 0;
35
+ for (const c of constraints)
36
+ total += (c.weight ?? 1.0) * constraintLoss(lons, c);
37
+ return total;
38
+ }
39
+ function bodiesOf(constraints) {
40
+ const s = new Set();
41
+ for (const c of constraints) {
42
+ if (c.kind === "aspect") {
43
+ s.add(c.a);
44
+ s.add(c.b);
45
+ }
46
+ else
47
+ s.add(c.body);
48
+ }
49
+ return [...s].sort();
50
+ }
51
+ function involves(c, body) {
52
+ return c.kind === "aspect" ? (c.a === body || c.b === body) : c.body === body;
53
+ }
54
+ function bodyLoss(lons, body, constraints) {
55
+ let total = 0;
56
+ for (const c of constraints)
57
+ if (involves(c, body))
58
+ total += (c.weight ?? 1.0) * constraintLoss(lons, c);
59
+ return total;
60
+ }
61
+ /** Find body longitudes minimizing the weighted constraint loss. */
62
+ export function compileForm(constraints, opts = {}) {
63
+ const restarts = opts.restarts ?? 12;
64
+ const iters = opts.iters ?? 8;
65
+ const impossibleDeg = opts.impossibleDeg ?? 5.0;
66
+ const bodies = bodiesOf(constraints);
67
+ const n = Math.max(bodies.length, 1);
68
+ let best = null;
69
+ for (let r = 0; r < restarts; r++) {
70
+ const lons = {};
71
+ bodies.forEach((b, i) => { lons[b] = (((r * n + i + 1) * PHI) % 1.0) * 360.0; });
72
+ for (let it = 0; it < iters; it++) {
73
+ for (const b of bodies) {
74
+ let bestL = lons[b];
75
+ let bestE = bodyLoss(lons, b, constraints);
76
+ for (let i = 0; i < 360; i++) {
77
+ lons[b] = i;
78
+ const e = bodyLoss(lons, b, constraints);
79
+ if (e < bestE) {
80
+ bestE = e;
81
+ bestL = i;
82
+ }
83
+ }
84
+ for (let k = -20; k <= 20; k++) {
85
+ const cand = (((bestL + k * 0.05) % 360.0) + 360.0) % 360.0;
86
+ lons[b] = cand;
87
+ const e = bodyLoss(lons, b, constraints);
88
+ if (e < bestE) {
89
+ bestE = e;
90
+ bestL = cand;
91
+ }
92
+ }
93
+ lons[b] = bestL;
94
+ }
95
+ }
96
+ const e = formLoss(lons, constraints);
97
+ if (best === null || e < best.e)
98
+ best = { e, lons: { ...lons } };
99
+ }
100
+ const lons = best.lons;
101
+ let maxLoss = 0;
102
+ for (const c of constraints)
103
+ maxLoss = Math.max(maxLoss, constraintLoss(lons, c));
104
+ return {
105
+ longitudes: lons,
106
+ residual: best.e,
107
+ maxConstraintLoss: maxLoss,
108
+ impossible: maxLoss > impossibleDeg,
109
+ constraints: constraints.map((c) => ({ ...c, loss: constraintLoss(lons, c) })),
110
+ };
111
+ }
@@ -0,0 +1,26 @@
1
+ import { Engine, BodyId, Zodiac } from "./chart.js";
2
+ import { RankedMoment } from "./scan.js";
3
+ export declare const DEFAULT_BODIES: string[];
4
+ /** Flat vector [w*cos(lon), w*sin(lon), ...] for the given (longitude, weight)
5
+ * pairs, in order. */
6
+ export declare function featureVector(weightedLons: [number, number][]): number[];
7
+ /** Cosine similarity of two feature vectors, in [-1, 1]. */
8
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
9
+ export interface FeatureOptions {
10
+ bodies?: BodyId[];
11
+ weights?: Record<string, number>;
12
+ zodiac?: Zodiac;
13
+ }
14
+ /** Feature vector for the sky at jdUt over an ordered set of bodies. */
15
+ export declare function chartFeatures(engine: Engine, jdUt: number, opts?: FeatureOptions): number[];
16
+ /** Similarity between the sky at jdUt and a target feature vector. */
17
+ export declare function configurationFit(engine: Engine, jdUt: number, target: number[], opts?: FeatureOptions): number;
18
+ export interface SearchConfigOptions extends FeatureOptions {
19
+ start: number;
20
+ end: number;
21
+ step: number;
22
+ limit?: number;
23
+ }
24
+ /** Rank the instants in [start, end] by how closely the sky resembles `target`
25
+ * (a feature vector), best first. Realization search over the feature space. */
26
+ export declare function searchConfigurations(engine: Engine, target: number[], opts: SearchConfigOptions): RankedMoment[];
@@ -0,0 +1,57 @@
1
+ /**
2
+ * astroengine features -- a chart as a feature vector, similarity between
3
+ * charts, and search for when the sky most resembles a target configuration.
4
+ *
5
+ * Each body's ecliptic longitude is circular, so it contributes a unit-circle
6
+ * point (cos, sin), optionally weighted. Cosine similarity between two such
7
+ * vectors is a weighted mean of cos(delta-longitude) per body: 1 when the
8
+ * configurations coincide, falling off as bodies diverge. The deterministic
9
+ * substrate for matching, retrieving, and searching chart configurations.
10
+ * Mirrors the Python reference (astroengine/features.py); the golden pins them.
11
+ */
12
+ import { DEG } from "./core.js";
13
+ import { rankMoments } from "./scan.js";
14
+ export const DEFAULT_BODIES = ["sun", "moon", "mercury", "venus", "mars",
15
+ "jupiter", "saturn", "uranus", "neptune", "pluto"];
16
+ /** Flat vector [w*cos(lon), w*sin(lon), ...] for the given (longitude, weight)
17
+ * pairs, in order. */
18
+ export function featureVector(weightedLons) {
19
+ const out = [];
20
+ for (const [lon, w] of weightedLons) {
21
+ const r = lon * DEG;
22
+ out.push(w * Math.cos(r), w * Math.sin(r));
23
+ }
24
+ return out;
25
+ }
26
+ /** Cosine similarity of two feature vectors, in [-1, 1]. */
27
+ export function cosineSimilarity(a, b) {
28
+ let dot = 0, na = 0, nb = 0;
29
+ const n = Math.min(a.length, b.length);
30
+ for (let i = 0; i < n; i++) {
31
+ dot += a[i] * b[i];
32
+ na += a[i] * a[i];
33
+ nb += b[i] * b[i];
34
+ }
35
+ if (na === 0 || nb === 0)
36
+ return 0;
37
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
38
+ }
39
+ /** Feature vector for the sky at jdUt over an ordered set of bodies. */
40
+ export function chartFeatures(engine, jdUt, opts = {}) {
41
+ const bodies = opts.bodies ?? DEFAULT_BODIES;
42
+ const zodiac = opts.zodiac ?? "tropical";
43
+ const wl = bodies.map((b) => [
44
+ engine.longitude(b, jdUt, { zodiac }),
45
+ opts.weights?.[b] ?? 1.0,
46
+ ]);
47
+ return featureVector(wl);
48
+ }
49
+ /** Similarity between the sky at jdUt and a target feature vector. */
50
+ export function configurationFit(engine, jdUt, target, opts = {}) {
51
+ return cosineSimilarity(chartFeatures(engine, jdUt, opts), target);
52
+ }
53
+ /** Rank the instants in [start, end] by how closely the sky resembles `target`
54
+ * (a feature vector), best first. Realization search over the feature space. */
55
+ export function searchConfigurations(engine, target, opts) {
56
+ return rankMoments({ start: opts.start, end: opts.end, step: opts.step, limit: opts.limit }, (jd) => configurationFit(engine, jd, target, opts));
57
+ }
@@ -13,3 +13,5 @@ export * from "./scan.js";
13
13
  export * from "./spherical.js";
14
14
  export * from "./astrocartography.js";
15
15
  export * from "./ephemeris.js";
16
+ export * from "./features.js";
17
+ export * from "./compiler.js";
package/dist/src/index.js CHANGED
@@ -13,3 +13,5 @@ export * from "./scan.js";
13
13
  export * from "./spherical.js";
14
14
  export * from "./astrocartography.js";
15
15
  export * from "./ephemeris.js";
16
+ export * from "./features.js";
17
+ export * from "./compiler.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caelus",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",