caelus 0.10.0 → 0.12.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.
@@ -44,8 +44,29 @@ function bisect(f, a, b, iters = 45) {
44
44
  }
45
45
  return (a + b) / 2;
46
46
  }
47
- /** Next rise/set/meridian transit (UT JD) after jdStart, or null when the
48
- * event does not occur in the window (polar day/night). */
47
+ /**
48
+ * The next rise, set, or meridian transit of a body after `jdStart`, as a
49
+ * Julian Day (UT). Accounts for the body's apparent radius, atmospheric
50
+ * refraction, and observer altitude.
51
+ *
52
+ * @param engine The engine used to evaluate positions.
53
+ * @param body A body id from {@link Engine.bodies}.
54
+ * @param jdStart Search start, Julian Day (UT). The result is the first event
55
+ * strictly after this instant.
56
+ * @param latDeg Observer latitude in degrees, north positive.
57
+ * @param lonDeg Observer longitude in degrees, east positive.
58
+ * @param kind `"rise"`, `"set"`, `"mtransit"` (upper/meridian transit), or
59
+ * `"itransit"` (lower transit). Defaults to `"rise"`.
60
+ * @param opts `altM` (observer altitude, m), `pressure` (hPa), `tempC`, and
61
+ * `searchDays` (how far ahead to look; defaults to 2).
62
+ * @returns The event time as a Julian Day (UT), or `null` when it does not
63
+ * occur in the window (e.g. polar day or night).
64
+ * @example
65
+ * ```ts
66
+ * // Next sunrise over London after 2025-06-01
67
+ * const jd = riseSet(engine, "sun", julianDay(2025, 6, 1), 51.5, -0.13, "rise");
68
+ * ```
69
+ */
49
70
  export function riseSet(engine, body, jdStart, latDeg, lonDeg, kind = "rise", opts = {}) {
50
71
  const altM = opts.altM ?? 0.0;
51
72
  const pressure = opts.pressure ?? 1013.25;
@@ -110,7 +131,23 @@ export function crossings(engine, body, targetLon, jdStart, jdEnd, zodiac = "tro
110
131
  }
111
132
  return out;
112
133
  }
113
- /** New/first-quarter/full/last-quarter times in [jdStart, jdEnd], sorted. */
134
+ /**
135
+ * Every principal lunar phase (new, first quarter, full, last quarter) within
136
+ * `[jdStart, jdEnd]`, sorted by time. Found from the Sun–Moon elongation
137
+ * crossing 0°/90°/180°/270°.
138
+ *
139
+ * @param engine The engine used to evaluate positions.
140
+ * @param jdStart Start of the window, Julian Day (UT).
141
+ * @param jdEnd End of the window, Julian Day (UT).
142
+ * @param maxHits Cap on the number of phases returned. Defaults to 60.
143
+ * @returns Sorted `[jdUt, phase]` pairs, where `phase` is one of
144
+ * {@link PhaseName}.
145
+ * @example
146
+ * ```ts
147
+ * const phases = lunarPhases(engine, julianDay(2025, 1, 1), julianDay(2025, 2, 1));
148
+ * // [[jd, "new"], [jd, "first_quarter"], ...]
149
+ * ```
150
+ */
114
151
  export function lunarPhases(engine, jdStart, jdEnd, maxHits = 60) {
115
152
  const elong = (t) => mod(engine.longitude("moon", t) - engine.longitude("sun", t), 360);
116
153
  const names = [
@@ -154,10 +191,19 @@ export function stations(engine, body, jdStart, jdEnd, maxHits = 30) {
154
191
  }
155
192
  return out;
156
193
  }
157
- /** Gauquelin sector (1..36, float) from rise/set times of the disc center
158
- * with refraction (Swiss Ephemeris method 3). Sectors run from rise: 1-18
159
- * above the horizon, 19-36 below. Null in polar no-rise/no-set
160
- * conditions. */
194
+ /**
195
+ * The Gauquelin sector of a body (1–36, fractional) from the rise/set times of
196
+ * the disc centre with refraction (Swiss Ephemeris method 3). Sectors run from
197
+ * rise: 1–18 above the horizon, 19–36 below.
198
+ *
199
+ * @param engine The engine used to evaluate positions.
200
+ * @param body A body id from {@link Engine.bodies}.
201
+ * @param jdUt Julian Day (UT).
202
+ * @param latDeg Observer latitude in degrees, north positive.
203
+ * @param lonDeg Observer longitude in degrees, east positive.
204
+ * @returns The sector in `[1, 37)`, or `null` in polar no-rise/no-set
205
+ * conditions.
206
+ */
161
207
  export function gauquelinSector(engine, body, jdUt, latDeg, lonDeg) {
162
208
  const surrounding = (kind) => {
163
209
  let t = riseSet(engine, body, jdUt - 1.3, latDeg, lonDeg, kind, { discCenter: true });
@@ -0,0 +1,88 @@
1
+ import { Engine, BodyId, Zodiac } from "./chart.js";
2
+ import { RankedMoment } from "./scan.js";
3
+ export declare const DEFAULT_BODIES: string[];
4
+ /**
5
+ * Build a feature vector from explicit `(longitude, weight)` pairs: each pair
6
+ * contributes a weighted unit-circle point `[w·cos(lon), w·sin(lon)]`. The
7
+ * low-level primitive behind {@link chartFeatures}; most callers want that.
8
+ *
9
+ * @param weightedLons `[longitudeDeg, weight]` pairs, in the order they should
10
+ * appear in the vector.
11
+ * @returns A flat vector, two entries per pair.
12
+ */
13
+ export declare function featureVector(weightedLons: [number, number][]): number[];
14
+ /**
15
+ * Cosine similarity of two feature vectors, in `[-1, 1]`. For vectors from
16
+ * {@link chartFeatures} this is a weighted mean of `cos(Δlongitude)` per body:
17
+ * `1` when the configurations coincide, falling off as bodies diverge.
18
+ *
19
+ * @param a First feature vector.
20
+ * @param b Second feature vector (compared over the shorter length).
21
+ * @returns Similarity in `[-1, 1]`; `0` if either vector is all zeros.
22
+ */
23
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
24
+ export interface FeatureOptions {
25
+ bodies?: BodyId[];
26
+ weights?: Record<string, number>;
27
+ zodiac?: Zodiac;
28
+ }
29
+ /**
30
+ * Encode the sky at an instant as a feature vector: each body's ecliptic
31
+ * longitude becomes a weighted unit-circle point. The deterministic substrate
32
+ * for matching and searching chart configurations — compare two with
33
+ * {@link cosineSimilarity}, or rank a time range against one with
34
+ * {@link searchConfigurations}.
35
+ *
36
+ * @param engine The engine used to evaluate positions.
37
+ * @param jdUt Julian Day in UT.
38
+ * @param opts `bodies` (ordered; defaults to the ten major bodies), per-body
39
+ * `weights`, and `zodiac` (tropical by default).
40
+ * @returns A flat vector `[w·cos(lon), w·sin(lon), ...]`, two entries per body
41
+ * in `bodies` order.
42
+ * @example
43
+ * ```ts
44
+ * const target = chartFeatures(engine, julianDay(2000, 1, 1));
45
+ * const now = chartFeatures(engine, julianDay(2025, 6, 1));
46
+ * cosineSimilarity(now, target); // 1 = identical configuration
47
+ * ```
48
+ */
49
+ export declare function chartFeatures(engine: Engine, jdUt: number, opts?: FeatureOptions): number[];
50
+ /**
51
+ * Similarity between the sky at `jdUt` and a target feature vector — shorthand
52
+ * for `cosineSimilarity(chartFeatures(engine, jdUt, opts), target)`. The scoring
53
+ * function {@link searchConfigurations} maximizes.
54
+ *
55
+ * @param engine The engine used to evaluate positions.
56
+ * @param jdUt Julian Day in UT.
57
+ * @param target A target feature vector from {@link chartFeatures}.
58
+ * @param opts {@link FeatureOptions} — must match those used to build `target`.
59
+ * @returns Cosine similarity in `[-1, 1]`.
60
+ */
61
+ export declare function configurationFit(engine: Engine, jdUt: number, target: number[], opts?: FeatureOptions): number;
62
+ export interface SearchConfigOptions extends FeatureOptions {
63
+ start: number;
64
+ end: number;
65
+ step: number;
66
+ limit?: number;
67
+ }
68
+ /**
69
+ * Rank the instants in `[start, end]` by how closely the sky resembles a
70
+ * `target` feature vector, best first — a realization search over the feature
71
+ * space. Build `target` with {@link chartFeatures} (e.g. from a natal chart).
72
+ *
73
+ * @param engine The engine used to evaluate positions.
74
+ * @param target A target feature vector from {@link chartFeatures}.
75
+ * @param opts `start`/`end` (Julian Days, UT) and `step` (days) define the
76
+ * scan, `limit` caps the results, plus the {@link FeatureOptions} (`bodies`,
77
+ * `weights`, `zodiac`) — which must match those used to build `target`.
78
+ * @returns Ranked `{ jd, score }` moments, highest similarity first.
79
+ * @example
80
+ * ```ts
81
+ * const natal = chartFeatures(engine, julianDay(1990, 6, 10, 14, 30));
82
+ * const matches = searchConfigurations(engine, natal, {
83
+ * start: julianDay(2025, 1, 1), end: julianDay(2026, 1, 1), step: 1, limit: 5,
84
+ * });
85
+ * matches[0].jd; // best-matching instant
86
+ * ```
87
+ */
88
+ export declare function searchConfigurations(engine: Engine, target: number[], opts: SearchConfigOptions): RankedMoment[];
@@ -0,0 +1,119 @@
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
+ /**
17
+ * Build a feature vector from explicit `(longitude, weight)` pairs: each pair
18
+ * contributes a weighted unit-circle point `[w·cos(lon), w·sin(lon)]`. The
19
+ * low-level primitive behind {@link chartFeatures}; most callers want that.
20
+ *
21
+ * @param weightedLons `[longitudeDeg, weight]` pairs, in the order they should
22
+ * appear in the vector.
23
+ * @returns A flat vector, two entries per pair.
24
+ */
25
+ export function featureVector(weightedLons) {
26
+ const out = [];
27
+ for (const [lon, w] of weightedLons) {
28
+ const r = lon * DEG;
29
+ out.push(w * Math.cos(r), w * Math.sin(r));
30
+ }
31
+ return out;
32
+ }
33
+ /**
34
+ * Cosine similarity of two feature vectors, in `[-1, 1]`. For vectors from
35
+ * {@link chartFeatures} this is a weighted mean of `cos(Δlongitude)` per body:
36
+ * `1` when the configurations coincide, falling off as bodies diverge.
37
+ *
38
+ * @param a First feature vector.
39
+ * @param b Second feature vector (compared over the shorter length).
40
+ * @returns Similarity in `[-1, 1]`; `0` if either vector is all zeros.
41
+ */
42
+ export function cosineSimilarity(a, b) {
43
+ let dot = 0, na = 0, nb = 0;
44
+ const n = Math.min(a.length, b.length);
45
+ for (let i = 0; i < n; i++) {
46
+ dot += a[i] * b[i];
47
+ na += a[i] * a[i];
48
+ nb += b[i] * b[i];
49
+ }
50
+ if (na === 0 || nb === 0)
51
+ return 0;
52
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
53
+ }
54
+ /**
55
+ * Encode the sky at an instant as a feature vector: each body's ecliptic
56
+ * longitude becomes a weighted unit-circle point. The deterministic substrate
57
+ * for matching and searching chart configurations — compare two with
58
+ * {@link cosineSimilarity}, or rank a time range against one with
59
+ * {@link searchConfigurations}.
60
+ *
61
+ * @param engine The engine used to evaluate positions.
62
+ * @param jdUt Julian Day in UT.
63
+ * @param opts `bodies` (ordered; defaults to the ten major bodies), per-body
64
+ * `weights`, and `zodiac` (tropical by default).
65
+ * @returns A flat vector `[w·cos(lon), w·sin(lon), ...]`, two entries per body
66
+ * in `bodies` order.
67
+ * @example
68
+ * ```ts
69
+ * const target = chartFeatures(engine, julianDay(2000, 1, 1));
70
+ * const now = chartFeatures(engine, julianDay(2025, 6, 1));
71
+ * cosineSimilarity(now, target); // 1 = identical configuration
72
+ * ```
73
+ */
74
+ export function chartFeatures(engine, jdUt, opts = {}) {
75
+ const bodies = opts.bodies ?? DEFAULT_BODIES;
76
+ const zodiac = opts.zodiac ?? "tropical";
77
+ const wl = bodies.map((b) => [
78
+ engine.longitude(b, jdUt, { zodiac }),
79
+ opts.weights?.[b] ?? 1.0,
80
+ ]);
81
+ return featureVector(wl);
82
+ }
83
+ /**
84
+ * Similarity between the sky at `jdUt` and a target feature vector — shorthand
85
+ * for `cosineSimilarity(chartFeatures(engine, jdUt, opts), target)`. The scoring
86
+ * function {@link searchConfigurations} maximizes.
87
+ *
88
+ * @param engine The engine used to evaluate positions.
89
+ * @param jdUt Julian Day in UT.
90
+ * @param target A target feature vector from {@link chartFeatures}.
91
+ * @param opts {@link FeatureOptions} — must match those used to build `target`.
92
+ * @returns Cosine similarity in `[-1, 1]`.
93
+ */
94
+ export function configurationFit(engine, jdUt, target, opts = {}) {
95
+ return cosineSimilarity(chartFeatures(engine, jdUt, opts), target);
96
+ }
97
+ /**
98
+ * Rank the instants in `[start, end]` by how closely the sky resembles a
99
+ * `target` feature vector, best first — a realization search over the feature
100
+ * space. Build `target` with {@link chartFeatures} (e.g. from a natal chart).
101
+ *
102
+ * @param engine The engine used to evaluate positions.
103
+ * @param target A target feature vector from {@link chartFeatures}.
104
+ * @param opts `start`/`end` (Julian Days, UT) and `step` (days) define the
105
+ * scan, `limit` caps the results, plus the {@link FeatureOptions} (`bodies`,
106
+ * `weights`, `zodiac`) — which must match those used to build `target`.
107
+ * @returns Ranked `{ jd, score }` moments, highest similarity first.
108
+ * @example
109
+ * ```ts
110
+ * const natal = chartFeatures(engine, julianDay(1990, 6, 10, 14, 30));
111
+ * const matches = searchConfigurations(engine, natal, {
112
+ * start: julianDay(2025, 1, 1), end: julianDay(2026, 1, 1), step: 1, limit: 5,
113
+ * });
114
+ * matches[0].jd; // best-matching instant
115
+ * ```
116
+ */
117
+ export function searchConfigurations(engine, target, opts) {
118
+ return rankMoments({ start: opts.start, end: opts.end, step: opts.step, limit: opts.limit }, (jd) => configurationFit(engine, jd, target, opts));
119
+ }
@@ -41,9 +41,18 @@ export declare function housesPolichPage(armc: number, phi: number, eps: number)
41
41
  /** Vehlow: equal houses with the ASC at the middle of house 1. */
42
42
  export declare function housesVehlow(armc: number, phi: number, eps: number): number[];
43
43
  /**
44
- * Placidus cusps via the classic iterative scheme. Semi-arc derivation:
45
- * for ALL four intermediate cusps RA = ARMC + offset + f*AD with
46
- * AD = asin(tan(phi) tan(dec)); offsets 30/60/120/150, f = 1/3,2/3,2/3,1/3.
47
- * Undefined above the polar circles (as Placidus itself is).
44
+ * Placidus house cusps via the classic iterative semi-arc scheme: for all four
45
+ * intermediate cusps RA = ARMC + offset + f·AD with AD = asin(tan φ · tan δ);
46
+ * offsets 30/60/120/150, f = 1/3, 2/3, 2/3, 1/3. Undefined above the polar
47
+ * circles, as Placidus itself is.
48
+ *
49
+ * Low-level: {@link Engine.chart} calls this for you when the house system is
50
+ * `"placidus"`, falling back to whole-sign near the poles. Inputs and outputs
51
+ * are in **radians**.
52
+ *
53
+ * @param armc Right ascension of the MC, in radians.
54
+ * @param phi Geographic latitude, in radians.
55
+ * @param eps Obliquity of the ecliptic, in radians.
56
+ * @returns The twelve cusp longitudes in radians, house 1 (Ascendant) first.
48
57
  */
49
58
  export declare function housesPlacidus(armc: number, phi: number, eps: number): number[];
@@ -257,10 +257,19 @@ export function housesVehlow(armc, phi, eps) {
257
257
  return Array.from({ length: 12 }, (_, i) => mod(asc - 15 * DEG + i * 30 * DEG, TWO_PI));
258
258
  }
259
259
  /**
260
- * Placidus cusps via the classic iterative scheme. Semi-arc derivation:
261
- * for ALL four intermediate cusps RA = ARMC + offset + f*AD with
262
- * AD = asin(tan(phi) tan(dec)); offsets 30/60/120/150, f = 1/3,2/3,2/3,1/3.
263
- * Undefined above the polar circles (as Placidus itself is).
260
+ * Placidus house cusps via the classic iterative semi-arc scheme: for all four
261
+ * intermediate cusps RA = ARMC + offset + f·AD with AD = asin(tan φ · tan δ);
262
+ * offsets 30/60/120/150, f = 1/3, 2/3, 2/3, 1/3. Undefined above the polar
263
+ * circles, as Placidus itself is.
264
+ *
265
+ * Low-level: {@link Engine.chart} calls this for you when the house system is
266
+ * `"placidus"`, falling back to whole-sign near the poles. Inputs and outputs
267
+ * are in **radians**.
268
+ *
269
+ * @param armc Right ascension of the MC, in radians.
270
+ * @param phi Geographic latitude, in radians.
271
+ * @param eps Obliquity of the ecliptic, in radians.
272
+ * @returns The twelve cusp longitudes in radians, house 1 (Ascendant) first.
264
273
  */
265
274
  export function housesPlacidus(armc, phi, eps) {
266
275
  const cusp = (offsetDeg, f) => {
@@ -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";
@@ -19,8 +19,22 @@ export interface Pheno {
19
19
  diameter: number;
20
20
  magnitude: number;
21
21
  }
22
- /** Phase angle (deg), illuminated fraction, elongation (deg), apparent
23
- * diameter (deg), apparent magnitude. */
22
+ /**
23
+ * Photometric and apparent-geometry quantities for a body at an instant: its
24
+ * phase angle, illuminated fraction, elongation from the Sun, apparent disc
25
+ * diameter, and apparent visual magnitude.
26
+ *
27
+ * @param engine The engine used to evaluate positions.
28
+ * @param body A body with known physical dimensions (Sun, Moon, the planets).
29
+ * @param jdUt Julian Day (UT).
30
+ * @returns A {@link Pheno}: `phaseAngle` (deg), `phase` (lit fraction `0`–`1`),
31
+ * `elongation` (deg), `diameter` (deg), and `magnitude`.
32
+ * @throws Error if `body` has no photometric data.
33
+ * @example
34
+ * ```ts
35
+ * pheno(engine, "venus", julianDay(2025, 6, 1)).phase; // illuminated fraction
36
+ * ```
37
+ */
24
38
  export declare function pheno(engine: Engine, body: BodyId, jdUt: number): Pheno;
25
39
  /** Apparent minus mean solar time, minutes (Meeus ch. 28). */
26
40
  export declare function equationOfTime(engine: Engine, jdUt: number): number;
package/dist/src/pheno.js CHANGED
@@ -66,8 +66,22 @@ function magnitude(body, a, r, dlt, jde, lonDeg, latDeg) {
66
66
  return x - 1.01;
67
67
  }
68
68
  }
69
- /** Phase angle (deg), illuminated fraction, elongation (deg), apparent
70
- * diameter (deg), apparent magnitude. */
69
+ /**
70
+ * Photometric and apparent-geometry quantities for a body at an instant: its
71
+ * phase angle, illuminated fraction, elongation from the Sun, apparent disc
72
+ * diameter, and apparent visual magnitude.
73
+ *
74
+ * @param engine The engine used to evaluate positions.
75
+ * @param body A body with known physical dimensions (Sun, Moon, the planets).
76
+ * @param jdUt Julian Day (UT).
77
+ * @returns A {@link Pheno}: `phaseAngle` (deg), `phase` (lit fraction `0`–`1`),
78
+ * `elongation` (deg), `diameter` (deg), and `magnitude`.
79
+ * @throws Error if `body` has no photometric data.
80
+ * @example
81
+ * ```ts
82
+ * pheno(engine, "venus", julianDay(2025, 6, 1)).phase; // illuminated fraction
83
+ * ```
84
+ */
71
85
  export function pheno(engine, body, jdUt) {
72
86
  if (DIAMETER_KM[body] === undefined) {
73
87
  throw new Error(`pheno not available for '${body}'`);
@@ -6,8 +6,26 @@ export interface Predicate {
6
6
  (engine: Engine, t: number): number;
7
7
  bodies: Set<string>;
8
8
  }
9
- /** True while `body` is within `orb` deg of an exact `kind` aspect to
10
- * `target` -- a fixed ecliptic longitude (deg) or another body name. */
9
+ /**
10
+ * A {@link Predicate} that holds while `body` is within `orb` degrees of an
11
+ * exact aspect to `target`. Feed it to {@link when} to find the time windows,
12
+ * or compose it with {@link allOf}/{@link anyOf}.
13
+ *
14
+ * @param body The transiting body.
15
+ * @param kind Aspect name, e.g. `"conjunction"`, `"square"`, `"trine"`,
16
+ * `"opposition"`, `"sextile"`.
17
+ * @param target The aspect target: a fixed ecliptic longitude in degrees (e.g.
18
+ * a natal point) or another body id (a mutual aspect).
19
+ * @param orb Half-width of the window in degrees. Defaults to `1.0`.
20
+ * @param zodiac Zodiac for the longitudes. Defaults to tropical.
21
+ * @returns A predicate, true while within orb of the exact aspect.
22
+ * @throws Error if `kind` is not a known aspect.
23
+ * @example
24
+ * ```ts
25
+ * const natalSun = 79.3;
26
+ * when(engine, aspect("saturn", "square", natalSun, 1), jd0, jd1); // Saturn squares
27
+ * ```
28
+ */
11
29
  export declare function aspect(body: BodyId, kind: string, target: number | BodyId, orb?: number, zodiac?: Zodiac): Predicate;
12
30
  /** True while `body` is in `sign` (index 0=Aries..11=Pisces, or name). */
13
31
  export declare function inSign(body: BodyId, sign: number | string, zodiac?: Zodiac): Predicate;
@@ -25,8 +43,31 @@ export interface WhenOptions {
25
43
  step?: number;
26
44
  maxIntervals?: number;
27
45
  }
28
- /** Time intervals (jdStartUt, jdEndUt) in [jdStart, jdEnd] where `predicate`
29
- * is true. Endpoints touching the range bounds are clamped. The scan step
30
- * defaults to 0.125 d when a fast body (Moon, nodes, Lilith) is involved
31
- * and 1 d otherwise. */
46
+ /**
47
+ * Solve for the time intervals within `[jdStart, jdEnd]` (UT Julian Days) where
48
+ * a {@link Predicate} holds. Predicates compose from {@link aspect},
49
+ * {@link inSign}, {@link retrograde}, {@link notRetrograde}, and the
50
+ * {@link allOf}/{@link anyOf} combinators, so one call answers questions like
51
+ * "when is Venus in Taurus while Mercury is direct?".
52
+ *
53
+ * Returned intervals are sorted and disjoint; endpoints touching the range
54
+ * bounds are clamped. The scan step defaults to 0.125 d when a fast body (Moon,
55
+ * nodes, Lilith) is involved and 1 d otherwise — override it with `opts.step`.
56
+ *
57
+ * @param engine The engine used to evaluate positions.
58
+ * @param predicate A celestial predicate (see {@link aspect}, {@link inSign}).
59
+ * @param jdStart Start of the search window, Julian Day (UT).
60
+ * @param jdEnd End of the search window, Julian Day (UT).
61
+ * @param opts `step` (scan resolution in days) and `maxIntervals`.
62
+ * @returns Sorted, disjoint `[startUt, endUt]` intervals where the predicate is
63
+ * true.
64
+ * @example
65
+ * ```ts
66
+ * const windows = when(
67
+ * engine,
68
+ * allOf(inSign("venus", "Taurus"), notRetrograde("mercury")),
69
+ * julianDay(2025, 1, 1), julianDay(2026, 1, 1),
70
+ * );
71
+ * ```
72
+ */
32
73
  export declare function when(engine: Engine, predicate: Predicate, jdStart: number, jdEnd: number, opts?: WhenOptions): Interval[];
package/dist/src/query.js CHANGED
@@ -35,8 +35,26 @@ function mk(fn, bodies) {
35
35
  return p;
36
36
  }
37
37
  // ---------------------------------------------------------------- predicates
38
- /** True while `body` is within `orb` deg of an exact `kind` aspect to
39
- * `target` -- a fixed ecliptic longitude (deg) or another body name. */
38
+ /**
39
+ * A {@link Predicate} that holds while `body` is within `orb` degrees of an
40
+ * exact aspect to `target`. Feed it to {@link when} to find the time windows,
41
+ * or compose it with {@link allOf}/{@link anyOf}.
42
+ *
43
+ * @param body The transiting body.
44
+ * @param kind Aspect name, e.g. `"conjunction"`, `"square"`, `"trine"`,
45
+ * `"opposition"`, `"sextile"`.
46
+ * @param target The aspect target: a fixed ecliptic longitude in degrees (e.g.
47
+ * a natal point) or another body id (a mutual aspect).
48
+ * @param orb Half-width of the window in degrees. Defaults to `1.0`.
49
+ * @param zodiac Zodiac for the longitudes. Defaults to tropical.
50
+ * @returns A predicate, true while within orb of the exact aspect.
51
+ * @throws Error if `kind` is not a known aspect.
52
+ * @example
53
+ * ```ts
54
+ * const natalSun = 79.3;
55
+ * when(engine, aspect("saturn", "square", natalSun, 1), jd0, jd1); // Saturn squares
56
+ * ```
57
+ */
40
58
  export function aspect(body, kind, target, orb = 1.0, zodiac = "tropical") {
41
59
  const ang = QUERY_ASPECTS[kind];
42
60
  if (ang === undefined)
@@ -117,10 +135,33 @@ function bisect(f, a, b, tol = 1e-6) {
117
135
  }
118
136
  return 0.5 * (a + b);
119
137
  }
120
- /** Time intervals (jdStartUt, jdEndUt) in [jdStart, jdEnd] where `predicate`
121
- * is true. Endpoints touching the range bounds are clamped. The scan step
122
- * defaults to 0.125 d when a fast body (Moon, nodes, Lilith) is involved
123
- * and 1 d otherwise. */
138
+ /**
139
+ * Solve for the time intervals within `[jdStart, jdEnd]` (UT Julian Days) where
140
+ * a {@link Predicate} holds. Predicates compose from {@link aspect},
141
+ * {@link inSign}, {@link retrograde}, {@link notRetrograde}, and the
142
+ * {@link allOf}/{@link anyOf} combinators, so one call answers questions like
143
+ * "when is Venus in Taurus while Mercury is direct?".
144
+ *
145
+ * Returned intervals are sorted and disjoint; endpoints touching the range
146
+ * bounds are clamped. The scan step defaults to 0.125 d when a fast body (Moon,
147
+ * nodes, Lilith) is involved and 1 d otherwise — override it with `opts.step`.
148
+ *
149
+ * @param engine The engine used to evaluate positions.
150
+ * @param predicate A celestial predicate (see {@link aspect}, {@link inSign}).
151
+ * @param jdStart Start of the search window, Julian Day (UT).
152
+ * @param jdEnd End of the search window, Julian Day (UT).
153
+ * @param opts `step` (scan resolution in days) and `maxIntervals`.
154
+ * @returns Sorted, disjoint `[startUt, endUt]` intervals where the predicate is
155
+ * true.
156
+ * @example
157
+ * ```ts
158
+ * const windows = when(
159
+ * engine,
160
+ * allOf(inSign("venus", "Taurus"), notRetrograde("mercury")),
161
+ * julianDay(2025, 1, 1), julianDay(2026, 1, 1),
162
+ * );
163
+ * ```
164
+ */
124
165
  export function when(engine, predicate, jdStart, jdEnd, opts = {}) {
125
166
  let step = opts.step;
126
167
  if (step === undefined) {
@@ -9,12 +9,46 @@ export interface TurboPack {
9
9
  zodiac: string;
10
10
  bodies: Record<string, TurboBody>;
11
11
  }
12
+ /**
13
+ * Runtime evaluator for a **turbo pack** — a segmented Chebyshev fit of the
14
+ * engine's apparent longitude over a fixed range and body set. Evaluating a
15
+ * longitude costs a couple dozen multiply-adds, so a century-scale transit scan
16
+ * that calls it tens of thousands of times runs in milliseconds. Construct one
17
+ * from a pack you minted offline; it does no fitting, no I/O, and needs no
18
+ * {@link Engine}.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const turbo = new Turbo(pack); // a TurboPack generated for your range/bodies
23
+ * if (turbo.has("mars")) turbo.longitude("mars", jd);
24
+ * ```
25
+ */
12
26
  export declare class Turbo {
27
+ /** Start of the pack's valid Julian Day (UT) range. */
13
28
  readonly jd0: number;
29
+ /** End of the pack's valid Julian Day (UT) range. */
14
30
  readonly jd1: number;
15
31
  private readonly bodies;
32
+ /**
33
+ * @param pack A {@link TurboPack}: the fitted segments plus its `jd0`/`jd1`
34
+ * range, minted offline for your bodies and span.
35
+ */
16
36
  constructor(pack: TurboPack);
37
+ /**
38
+ * Whether this pack can evaluate a given body.
39
+ *
40
+ * @param body Body id to test.
41
+ * @returns `true` if {@link Turbo.longitude} accepts `body`.
42
+ */
17
43
  has(body: string): boolean;
18
- /** Apparent ecliptic longitude (degrees) from the turbo pack. */
44
+ /**
45
+ * Apparent ecliptic longitude (degrees) of a body from the turbo pack, in the
46
+ * pack's own zodiac. The hot path for bulk scans.
47
+ *
48
+ * @param body A body id the pack contains (see {@link Turbo.has}).
49
+ * @param jd Julian Day (UT), within `[jd0, jd1]`.
50
+ * @returns Ecliptic longitude in degrees, `[0, 360)`.
51
+ * @throws Error if the pack lacks `body`, or `jd` is outside `[jd0, jd1]`.
52
+ */
19
53
  longitude(body: string, jd: number): number;
20
54
  }
package/dist/src/turbo.js CHANGED
@@ -22,19 +22,53 @@ function clenshaw(coeffs, x) {
22
22
  }
23
23
  return x * b0 - b1 + coeffs[0];
24
24
  }
25
+ /**
26
+ * Runtime evaluator for a **turbo pack** — a segmented Chebyshev fit of the
27
+ * engine's apparent longitude over a fixed range and body set. Evaluating a
28
+ * longitude costs a couple dozen multiply-adds, so a century-scale transit scan
29
+ * that calls it tens of thousands of times runs in milliseconds. Construct one
30
+ * from a pack you minted offline; it does no fitting, no I/O, and needs no
31
+ * {@link Engine}.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const turbo = new Turbo(pack); // a TurboPack generated for your range/bodies
36
+ * if (turbo.has("mars")) turbo.longitude("mars", jd);
37
+ * ```
38
+ */
25
39
  export class Turbo {
40
+ /** Start of the pack's valid Julian Day (UT) range. */
26
41
  jd0;
42
+ /** End of the pack's valid Julian Day (UT) range. */
27
43
  jd1;
28
44
  bodies;
45
+ /**
46
+ * @param pack A {@link TurboPack}: the fitted segments plus its `jd0`/`jd1`
47
+ * range, minted offline for your bodies and span.
48
+ */
29
49
  constructor(pack) {
30
50
  this.jd0 = pack.jd0;
31
51
  this.jd1 = pack.jd1;
32
52
  this.bodies = pack.bodies;
33
53
  }
54
+ /**
55
+ * Whether this pack can evaluate a given body.
56
+ *
57
+ * @param body Body id to test.
58
+ * @returns `true` if {@link Turbo.longitude} accepts `body`.
59
+ */
34
60
  has(body) {
35
61
  return body in this.bodies;
36
62
  }
37
- /** Apparent ecliptic longitude (degrees) from the turbo pack. */
63
+ /**
64
+ * Apparent ecliptic longitude (degrees) of a body from the turbo pack, in the
65
+ * pack's own zodiac. The hot path for bulk scans.
66
+ *
67
+ * @param body A body id the pack contains (see {@link Turbo.has}).
68
+ * @param jd Julian Day (UT), within `[jd0, jd1]`.
69
+ * @returns Ecliptic longitude in degrees, `[0, 360)`.
70
+ * @throws Error if the pack lacks `body`, or `jd` is outside `[jd0, jd1]`.
71
+ */
38
72
  longitude(body, jd) {
39
73
  const b = this.bodies[body];
40
74
  if (!b)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caelus",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",