caelus 0.21.0 → 0.23.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 +5 -3
- package/accuracy.json +2 -2
- package/data/constellations.json +1 -0
- package/data/fixed_stars_deep.json +1 -0
- package/dist/data/constellations.json +1 -0
- package/dist/src/chart.d.ts +30 -1
- package/dist/src/chart.js +45 -3
- package/dist/src/core.d.ts +5 -0
- package/dist/src/data-embedded.js +2 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/interpretation.js +2 -0
- package/dist/src/node-loader.js +6 -0
- package/dist/src/skyview.d.ts +351 -0
- package/dist/src/skyview.js +1002 -0
- package/dist/src/stars.d.ts +15 -0
- package/dist/src/synthetic.d.ts +249 -0
- package/dist/src/synthetic.js +260 -0
- package/package.json +6 -4
package/dist/src/stars.d.ts
CHANGED
|
@@ -24,5 +24,20 @@ export interface StarPack {
|
|
|
24
24
|
frame: string;
|
|
25
25
|
stars: Record<string, StarEntry>;
|
|
26
26
|
}
|
|
27
|
+
/** Constellation figure lines and labels; vertices as ecliptic J2000 (lon, lat)
|
|
28
|
+
* degrees. Built by scripts/build-constellations.mjs from d3-celestial. */
|
|
29
|
+
export interface ConstellationPack {
|
|
30
|
+
provenance: string;
|
|
31
|
+
lines: {
|
|
32
|
+
con: string;
|
|
33
|
+
segs: number[][][];
|
|
34
|
+
}[];
|
|
35
|
+
labels: {
|
|
36
|
+
name: string;
|
|
37
|
+
con: string;
|
|
38
|
+
lon: number;
|
|
39
|
+
lat: number;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
27
42
|
/** Apparent ecliptic [lon, lat] of date (rad) for a catalog entry. */
|
|
28
43
|
export declare function starApparent(data: EngineData, s: StarEntry, jde: number): [number, number];
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* caelus synthetic ephemeris -- give imaginary bodies motion, not just a
|
|
3
|
+
* single longitude.
|
|
4
|
+
*
|
|
5
|
+
* {@link compileForm} places an authored body at one static longitude
|
|
6
|
+
* (a snapshot). A sky that does not move is dead: no transits, no returns, no
|
|
7
|
+
* seasons. This module makes a body's position a deterministic **function of
|
|
8
|
+
* time** from authored parameters, so every derived computation — transits,
|
|
9
|
+
* returns, retrograde, conjunctions, phases, SkyView — works on it unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Three tiers, increasing in physics:
|
|
12
|
+
* - **placement** — a fixed longitude (parity with `compileForm`; effectively a
|
|
13
|
+
* body on the celestial sphere that never moves).
|
|
14
|
+
* - **periodic** — `lon(t) = wrap360(phaseDeg + 360·(t − epoch)/periodDays)`.
|
|
15
|
+
* Cheap, no real physics, but yields returns, cyclic transits, and (viewed
|
|
16
|
+
* from an inner observer body) apparent retrograde. Covers ~all
|
|
17
|
+
* narrative/game needs.
|
|
18
|
+
* - **kepler** — constant Keplerian elements solved each instant
|
|
19
|
+
* (`{ a, e, i, node, peri, M0, periodDays }`), giving heliocentric ecliptic
|
|
20
|
+
* (x, y, z, lon, lat, r); with a designated **observer body** the positions
|
|
21
|
+
* become geocentric/apparent, enabling true retrograde, phases and
|
|
22
|
+
* occultation.
|
|
23
|
+
*
|
|
24
|
+
* Determinism is non-negotiable: every export is a pure function of
|
|
25
|
+
* `(system, t)`. No randomness, no wall-clock reads — identical inputs yield
|
|
26
|
+
* identical outputs, like the rest of Caelus.
|
|
27
|
+
*
|
|
28
|
+
* Two ways to consume a system:
|
|
29
|
+
* 1. {@link syntheticPositions} / {@link syntheticEphemeris} — the self-
|
|
30
|
+
* contained *world frame*. An `observer` body, if set, is the vantage point;
|
|
31
|
+
* longitudes are in the world's own ecliptic, decoupled from Earth. This is
|
|
32
|
+
* the canonical position-over-time contract.
|
|
33
|
+
* 2. {@link registerSyntheticSystem} — drop the bodies into a real {@link
|
|
34
|
+
* Engine} via {@link Engine.registerSource}, so `transits`, `returns`,
|
|
35
|
+
* `skyView`, `relational` etc. consume them through their existing `Engine`
|
|
36
|
+
* interfaces with zero changes. Here the vantage point is real Earth (the
|
|
37
|
+
* system's heliocentre maps to the Sun), so the synthetic `observer` does
|
|
38
|
+
* not apply — it is "imaginary bodies in our sky".
|
|
39
|
+
*/
|
|
40
|
+
import { XyzSource } from "./core.js";
|
|
41
|
+
/** A single authored body. Its `id` is how every consumer (charts, transits,
|
|
42
|
+
* SkyView) refers to it. */
|
|
43
|
+
export type SyntheticBody =
|
|
44
|
+
/** A fixed longitude that never moves — parity with `compileForm`. */
|
|
45
|
+
{
|
|
46
|
+
id: string;
|
|
47
|
+
mode: "placement";
|
|
48
|
+
lonDeg: number;
|
|
49
|
+
}
|
|
50
|
+
/** Uniform angular motion: `lon(t) = phaseDeg + 360·(t − epoch)/periodDays`.
|
|
51
|
+
* `epoch` defaults to `0`. */
|
|
52
|
+
| {
|
|
53
|
+
id: string;
|
|
54
|
+
mode: "periodic";
|
|
55
|
+
periodDays: number;
|
|
56
|
+
phaseDeg: number;
|
|
57
|
+
epoch?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Constant Keplerian elements. Angles `i`, `node`, `peri`, `M0` in **degrees**;
|
|
60
|
+
* `a` in arbitrary length units (consistent within a system); `e` in `[0, 1)`;
|
|
61
|
+
* `epoch` defaults to `0`. */
|
|
62
|
+
| {
|
|
63
|
+
id: string;
|
|
64
|
+
mode: "kepler";
|
|
65
|
+
a: number;
|
|
66
|
+
e: number;
|
|
67
|
+
i: number;
|
|
68
|
+
node: number;
|
|
69
|
+
peri: number;
|
|
70
|
+
M0: number;
|
|
71
|
+
periodDays: number;
|
|
72
|
+
epoch?: number;
|
|
73
|
+
};
|
|
74
|
+
/** Render attributes for a synthetic body — how an imaginary body should *look*
|
|
75
|
+
* in a SkyView frame. The engine owns position; these own appearance, so a body
|
|
76
|
+
* stays visually consistent across {@link skyViewSequence} frames. */
|
|
77
|
+
export interface SyntheticRender {
|
|
78
|
+
/** Apparent angular diameter in degrees (the Moon is ~0.5°). */
|
|
79
|
+
sizeDeg?: number;
|
|
80
|
+
/** Apparent visual magnitude (smaller is brighter; Sirius is ~-1.5). */
|
|
81
|
+
magnitude?: number;
|
|
82
|
+
/** A colour hint, e.g. a CSS colour or evocative name ("pale gold"). */
|
|
83
|
+
color?: string;
|
|
84
|
+
}
|
|
85
|
+
/** An authored celestial system: a set of {@link SyntheticBody} that move
|
|
86
|
+
* together, an optional `observer` body that fixes the vantage point, and
|
|
87
|
+
* optional per-body {@link SyntheticRender} attributes. */
|
|
88
|
+
export interface SyntheticSystem {
|
|
89
|
+
id: string;
|
|
90
|
+
bodies: SyntheticBody[];
|
|
91
|
+
/** Body id of the vantage point. When set, {@link syntheticPositions} returns
|
|
92
|
+
* geocentric/apparent positions seen from this body (it sees itself at the
|
|
93
|
+
* origin, so it is reported at its own heliocentric place). When unset,
|
|
94
|
+
* positions are heliocentric. */
|
|
95
|
+
observer?: string;
|
|
96
|
+
/** Render attributes keyed by body id. */
|
|
97
|
+
render?: Record<string, SyntheticRender>;
|
|
98
|
+
}
|
|
99
|
+
/** A body's position at one instant, in degrees and system length units. */
|
|
100
|
+
export interface SyntheticPosition {
|
|
101
|
+
/** Ecliptic longitude in degrees, `[0, 360)`. */
|
|
102
|
+
lonDeg: number;
|
|
103
|
+
/** Ecliptic latitude in degrees. */
|
|
104
|
+
latDeg: number;
|
|
105
|
+
/** Distance from the vantage point (heliocentric, or from the observer body
|
|
106
|
+
* when one is set), in the system's length units. */
|
|
107
|
+
r: number;
|
|
108
|
+
}
|
|
109
|
+
/** Why a system is unsatisfiable, if it is — the `impossible`/`residual` honesty
|
|
110
|
+
* pattern, mirroring {@link CompiledForm}. A system with problems
|
|
111
|
+
* still computes for the bodies that are individually valid, but flags the rest
|
|
112
|
+
* rather than silently producing garbage. */
|
|
113
|
+
export interface SyntheticDiagnosis {
|
|
114
|
+
/** `true` when at least one problem makes the system ill-defined. */
|
|
115
|
+
impossible: boolean;
|
|
116
|
+
/** One human-readable line per problem (empty when the system is sound). */
|
|
117
|
+
problems: string[];
|
|
118
|
+
}
|
|
119
|
+
/** A pluggable position-over-time source: the same shape every derived
|
|
120
|
+
* computation needs from a body. Returned by {@link syntheticEphemeris}. */
|
|
121
|
+
export interface BodyPositionSource extends SyntheticDiagnosis {
|
|
122
|
+
/** The body ids this source can place. */
|
|
123
|
+
bodies(): string[];
|
|
124
|
+
/** Apparent ecliptic longitude (degrees, `[0, 360)`) at time `t`. */
|
|
125
|
+
longitude(id: string, t: number): number;
|
|
126
|
+
/** Full position at time `t`, with longitude speed and a retrograde flag
|
|
127
|
+
* derived by central difference — matching the {@link Position}
|
|
128
|
+
* contract that real bodies satisfy. */
|
|
129
|
+
position(id: string, t: number): SyntheticPosition & {
|
|
130
|
+
speed: number;
|
|
131
|
+
retrograde: boolean;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check an authored system for the ways it can be ill-defined — duplicate ids, a
|
|
136
|
+
* dangling `observer`, non-positive periods, or out-of-range eccentricity — and
|
|
137
|
+
* report each, in the `impossible`/`residual` honesty style. Pure.
|
|
138
|
+
*
|
|
139
|
+
* @param sys The system to check.
|
|
140
|
+
* @returns A {@link SyntheticDiagnosis}; `impossible` is `true` when any problem
|
|
141
|
+
* is found.
|
|
142
|
+
*/
|
|
143
|
+
export declare function validateSyntheticSystem(sys: SyntheticSystem): SyntheticDiagnosis;
|
|
144
|
+
/**
|
|
145
|
+
* The heliocentric xyz source for one authored body — the per-body engine behind
|
|
146
|
+
* every tier, and the exact object {@link Engine.registerSource} consumes.
|
|
147
|
+
*
|
|
148
|
+
* - **placement** sits on a very distant sphere so its direction (longitude) is
|
|
149
|
+
* fixed and parallax-free from any vantage.
|
|
150
|
+
* - **periodic** is a circular, coplanar orbit (`e = 0`, `i = 0`) whose radius
|
|
151
|
+
* follows Kepler's third law in the system's units (`a = periodDays^(2/3)`),
|
|
152
|
+
* so an inner body both moves faster and orbits tighter — making outer bodies
|
|
153
|
+
* show apparent retrograde near opposition, just like the real sky. Its
|
|
154
|
+
* heliocentric longitude is exactly `phaseDeg + 360·(t − epoch)/periodDays`.
|
|
155
|
+
* - **kepler** is the full constant-element solver ({@link KeplerOrbit}).
|
|
156
|
+
*
|
|
157
|
+
* @param body The authored body.
|
|
158
|
+
* @returns An {@link XyzSource} yielding heliocentric ecliptic xyz at time `t`.
|
|
159
|
+
*/
|
|
160
|
+
export declare function bodySource(body: SyntheticBody): XyzSource;
|
|
161
|
+
/**
|
|
162
|
+
* Build the heliocentric {@link XyzSource} for every body in a system, keyed by
|
|
163
|
+
* id — the raw material for both the world-frame contract and engine
|
|
164
|
+
* registration.
|
|
165
|
+
*
|
|
166
|
+
* @param sys The authored system.
|
|
167
|
+
* @returns One source per body id.
|
|
168
|
+
*/
|
|
169
|
+
export declare function syntheticSources(sys: SyntheticSystem): Record<string, XyzSource>;
|
|
170
|
+
/**
|
|
171
|
+
* Positions of every body at one instant — the core position-over-time
|
|
172
|
+
* contract. Pure: a function of `(sys, t)` only.
|
|
173
|
+
*
|
|
174
|
+
* When `sys.observer` is set, each other body's position is geocentric/apparent
|
|
175
|
+
* as seen from the observer body (its heliocentric vector subtracted), so an
|
|
176
|
+
* outer body shows a real apparent retrograde arc around opposition. The
|
|
177
|
+
* observer body itself is reported at its own heliocentric place. With no
|
|
178
|
+
* observer the positions are heliocentric.
|
|
179
|
+
*
|
|
180
|
+
* @param sys The authored system.
|
|
181
|
+
* @param tDays The instant, in the same day units as each body's `periodDays`
|
|
182
|
+
* and `epoch`.
|
|
183
|
+
* @returns Position per body id (`lonDeg`, `latDeg`, `r`).
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* const sys: SyntheticSystem = {
|
|
187
|
+
* id: "twin-moons",
|
|
188
|
+
* bodies: [
|
|
189
|
+
* { id: "fast", mode: "periodic", periodDays: 20, phaseDeg: 0 },
|
|
190
|
+
* { id: "slow", mode: "periodic", periodDays: 80, phaseDeg: 0 },
|
|
191
|
+
* ],
|
|
192
|
+
* };
|
|
193
|
+
* syntheticPositions(sys, 0).fast.lonDeg; // 0
|
|
194
|
+
* syntheticPositions(sys, 5).fast.lonDeg; // 90 (a quarter of its 20-day period)
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export declare function syntheticPositions(sys: SyntheticSystem, tDays: number): Record<string, SyntheticPosition>;
|
|
198
|
+
/**
|
|
199
|
+
* A {@link BodyPositionSource} over an authored system: the world-frame view as
|
|
200
|
+
* a reusable object, with longitude speed and a retrograde flag so it satisfies
|
|
201
|
+
* the same contract real ephemeris bodies do. Carries the system's
|
|
202
|
+
* {@link validateSyntheticSystem} diagnosis (`impossible`/`problems`) — the
|
|
203
|
+
* honesty pattern — and still serves the bodies that are individually valid.
|
|
204
|
+
*
|
|
205
|
+
* @param sys The authored system.
|
|
206
|
+
* @returns A position source: `bodies()`, `longitude(id, t)`, `position(id, t)`,
|
|
207
|
+
* plus `impossible`/`problems`.
|
|
208
|
+
*/
|
|
209
|
+
export declare function syntheticEphemeris(sys: SyntheticSystem): BodyPositionSource;
|
|
210
|
+
/** A type guard for the surface {@link registerSyntheticSystem} needs, so the
|
|
211
|
+
* synthetic module does not import the heavy {@link Engine} class. */
|
|
212
|
+
export interface SourceRegistrar {
|
|
213
|
+
registerSource(id: string, source: XyzSource): unknown;
|
|
214
|
+
registerRender?(id: string, render: SyntheticRender): unknown;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Register a system's bodies on a real {@link Engine} so `transits`, `returns`,
|
|
218
|
+
* retrograde detection, `skyView`, `relational` — every computation built on
|
|
219
|
+
* `Engine.position`/`Engine.longitude` — consume them unchanged. The bodies are
|
|
220
|
+
* placed heliocentrically in the real solar system and viewed from real Earth,
|
|
221
|
+
* so the synthetic `observer` is *not* applied here (use
|
|
222
|
+
* {@link syntheticPositions} for the self-contained world frame). Each body's
|
|
223
|
+
* `epoch` is then interpreted as a TT Julian Day.
|
|
224
|
+
*
|
|
225
|
+
* @param engine The engine to register onto (anything with `registerSource`).
|
|
226
|
+
* @param sys The authored system.
|
|
227
|
+
* @returns The ids registered.
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* registerSyntheticSystem(engine, {
|
|
231
|
+
* id: "nemesis", bodies: [{ id: "nemesis", mode: "kepler", a: 520, e: 0.1,
|
|
232
|
+
* i: 5, node: 0, peri: 0, M0: 0, periodDays: 4_300_000, epoch: 2451545 }],
|
|
233
|
+
* });
|
|
234
|
+
* returns(engine, "nemesis", natalJd, jdStart, jdEnd); // works, zero changes
|
|
235
|
+
* engine.position("nemesis", jd).retrograde; // apparent, from Earth
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export declare function registerSyntheticSystem(engine: SourceRegistrar, sys: SyntheticSystem): string[];
|
|
239
|
+
/**
|
|
240
|
+
* The {@link SyntheticRender} attributes for a body id, or `undefined` when none
|
|
241
|
+
* were authored. The accessor SkyView/`skyViewSequence` reads so an imaginary
|
|
242
|
+
* body keeps a consistent size, brightness and colour across frames — the engine
|
|
243
|
+
* owns position, this owns appearance.
|
|
244
|
+
*
|
|
245
|
+
* @param sys The authored system.
|
|
246
|
+
* @param id A body id.
|
|
247
|
+
* @returns The render attributes, or `undefined`.
|
|
248
|
+
*/
|
|
249
|
+
export declare function syntheticRender(sys: SyntheticSystem, id: string): SyntheticRender | undefined;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* caelus synthetic ephemeris -- give imaginary bodies motion, not just a
|
|
3
|
+
* single longitude.
|
|
4
|
+
*
|
|
5
|
+
* {@link compileForm} places an authored body at one static longitude
|
|
6
|
+
* (a snapshot). A sky that does not move is dead: no transits, no returns, no
|
|
7
|
+
* seasons. This module makes a body's position a deterministic **function of
|
|
8
|
+
* time** from authored parameters, so every derived computation — transits,
|
|
9
|
+
* returns, retrograde, conjunctions, phases, SkyView — works on it unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Three tiers, increasing in physics:
|
|
12
|
+
* - **placement** — a fixed longitude (parity with `compileForm`; effectively a
|
|
13
|
+
* body on the celestial sphere that never moves).
|
|
14
|
+
* - **periodic** — `lon(t) = wrap360(phaseDeg + 360·(t − epoch)/periodDays)`.
|
|
15
|
+
* Cheap, no real physics, but yields returns, cyclic transits, and (viewed
|
|
16
|
+
* from an inner observer body) apparent retrograde. Covers ~all
|
|
17
|
+
* narrative/game needs.
|
|
18
|
+
* - **kepler** — constant Keplerian elements solved each instant
|
|
19
|
+
* (`{ a, e, i, node, peri, M0, periodDays }`), giving heliocentric ecliptic
|
|
20
|
+
* (x, y, z, lon, lat, r); with a designated **observer body** the positions
|
|
21
|
+
* become geocentric/apparent, enabling true retrograde, phases and
|
|
22
|
+
* occultation.
|
|
23
|
+
*
|
|
24
|
+
* Determinism is non-negotiable: every export is a pure function of
|
|
25
|
+
* `(system, t)`. No randomness, no wall-clock reads — identical inputs yield
|
|
26
|
+
* identical outputs, like the rest of Caelus.
|
|
27
|
+
*
|
|
28
|
+
* Two ways to consume a system:
|
|
29
|
+
* 1. {@link syntheticPositions} / {@link syntheticEphemeris} — the self-
|
|
30
|
+
* contained *world frame*. An `observer` body, if set, is the vantage point;
|
|
31
|
+
* longitudes are in the world's own ecliptic, decoupled from Earth. This is
|
|
32
|
+
* the canonical position-over-time contract.
|
|
33
|
+
* 2. {@link registerSyntheticSystem} — drop the bodies into a real {@link
|
|
34
|
+
* Engine} via {@link Engine.registerSource}, so `transits`, `returns`,
|
|
35
|
+
* `skyView`, `relational` etc. consume them through their existing `Engine`
|
|
36
|
+
* interfaces with zero changes. Here the vantage point is real Earth (the
|
|
37
|
+
* system's heliocentre maps to the Sun), so the synthetic `observer` does
|
|
38
|
+
* not apply — it is "imaginary bodies in our sky".
|
|
39
|
+
*/
|
|
40
|
+
import { DEG, mod, KeplerOrbit } from "./core.js";
|
|
41
|
+
const TWO_PI = 2 * Math.PI;
|
|
42
|
+
// ----------------------------------------------------------------- validation
|
|
43
|
+
/**
|
|
44
|
+
* Check an authored system for the ways it can be ill-defined — duplicate ids, a
|
|
45
|
+
* dangling `observer`, non-positive periods, or out-of-range eccentricity — and
|
|
46
|
+
* report each, in the `impossible`/`residual` honesty style. Pure.
|
|
47
|
+
*
|
|
48
|
+
* @param sys The system to check.
|
|
49
|
+
* @returns A {@link SyntheticDiagnosis}; `impossible` is `true` when any problem
|
|
50
|
+
* is found.
|
|
51
|
+
*/
|
|
52
|
+
export function validateSyntheticSystem(sys) {
|
|
53
|
+
const problems = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const b of sys.bodies) {
|
|
56
|
+
if (seen.has(b.id))
|
|
57
|
+
problems.push(`duplicate body id '${b.id}'`);
|
|
58
|
+
seen.add(b.id);
|
|
59
|
+
if (b.mode === "periodic" && !(b.periodDays > 0)) {
|
|
60
|
+
problems.push(`body '${b.id}': periodDays must be > 0 (got ${b.periodDays})`);
|
|
61
|
+
}
|
|
62
|
+
if (b.mode === "kepler") {
|
|
63
|
+
if (!(b.periodDays > 0))
|
|
64
|
+
problems.push(`body '${b.id}': periodDays must be > 0 (got ${b.periodDays})`);
|
|
65
|
+
if (!(b.a > 0))
|
|
66
|
+
problems.push(`body '${b.id}': a must be > 0 (got ${b.a})`);
|
|
67
|
+
if (!(b.e >= 0 && b.e < 1))
|
|
68
|
+
problems.push(`body '${b.id}': e must be in [0, 1) (got ${b.e})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (sys.observer !== undefined && !seen.has(sys.observer)) {
|
|
72
|
+
problems.push(`observer '${sys.observer}' is not a body in the system`);
|
|
73
|
+
}
|
|
74
|
+
return { impossible: problems.length > 0, problems };
|
|
75
|
+
}
|
|
76
|
+
// -------------------------------------------------------------------- sources
|
|
77
|
+
/**
|
|
78
|
+
* The heliocentric xyz source for one authored body — the per-body engine behind
|
|
79
|
+
* every tier, and the exact object {@link Engine.registerSource} consumes.
|
|
80
|
+
*
|
|
81
|
+
* - **placement** sits on a very distant sphere so its direction (longitude) is
|
|
82
|
+
* fixed and parallax-free from any vantage.
|
|
83
|
+
* - **periodic** is a circular, coplanar orbit (`e = 0`, `i = 0`) whose radius
|
|
84
|
+
* follows Kepler's third law in the system's units (`a = periodDays^(2/3)`),
|
|
85
|
+
* so an inner body both moves faster and orbits tighter — making outer bodies
|
|
86
|
+
* show apparent retrograde near opposition, just like the real sky. Its
|
|
87
|
+
* heliocentric longitude is exactly `phaseDeg + 360·(t − epoch)/periodDays`.
|
|
88
|
+
* - **kepler** is the full constant-element solver ({@link KeplerOrbit}).
|
|
89
|
+
*
|
|
90
|
+
* @param body The authored body.
|
|
91
|
+
* @returns An {@link XyzSource} yielding heliocentric ecliptic xyz at time `t`.
|
|
92
|
+
*/
|
|
93
|
+
export function bodySource(body) {
|
|
94
|
+
if (body.mode === "placement") {
|
|
95
|
+
const R = 1e9;
|
|
96
|
+
const lam = body.lonDeg * DEG;
|
|
97
|
+
const v = [R * Math.cos(lam), R * Math.sin(lam), 0];
|
|
98
|
+
return { xyz: () => v };
|
|
99
|
+
}
|
|
100
|
+
if (body.mode === "periodic") {
|
|
101
|
+
const els = {
|
|
102
|
+
a: Math.cbrt(body.periodDays * body.periodDays),
|
|
103
|
+
e: 0, i: 0, node: 0, peri: 0,
|
|
104
|
+
M0: body.phaseDeg * DEG, n: TWO_PI / body.periodDays,
|
|
105
|
+
};
|
|
106
|
+
return new KeplerOrbit(els, body.epoch ?? 0);
|
|
107
|
+
}
|
|
108
|
+
const els = {
|
|
109
|
+
a: body.a, e: body.e, i: body.i * DEG, node: body.node * DEG,
|
|
110
|
+
peri: body.peri * DEG, M0: body.M0 * DEG, n: TWO_PI / body.periodDays,
|
|
111
|
+
};
|
|
112
|
+
return new KeplerOrbit(els, body.epoch ?? 0);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build the heliocentric {@link XyzSource} for every body in a system, keyed by
|
|
116
|
+
* id — the raw material for both the world-frame contract and engine
|
|
117
|
+
* registration.
|
|
118
|
+
*
|
|
119
|
+
* @param sys The authored system.
|
|
120
|
+
* @returns One source per body id.
|
|
121
|
+
*/
|
|
122
|
+
export function syntheticSources(sys) {
|
|
123
|
+
const out = {};
|
|
124
|
+
for (const b of sys.bodies)
|
|
125
|
+
out[b.id] = bodySource(b);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
function vecToPos(v) {
|
|
129
|
+
const r = Math.hypot(v[0], v[1], v[2]);
|
|
130
|
+
return {
|
|
131
|
+
lonDeg: mod(Math.atan2(v[1], v[0]) / DEG, 360),
|
|
132
|
+
latDeg: r === 0 ? 0 : Math.asin(v[2] / r) / DEG,
|
|
133
|
+
r,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// ------------------------------------------------------------------- contract
|
|
137
|
+
/**
|
|
138
|
+
* Positions of every body at one instant — the core position-over-time
|
|
139
|
+
* contract. Pure: a function of `(sys, t)` only.
|
|
140
|
+
*
|
|
141
|
+
* When `sys.observer` is set, each other body's position is geocentric/apparent
|
|
142
|
+
* as seen from the observer body (its heliocentric vector subtracted), so an
|
|
143
|
+
* outer body shows a real apparent retrograde arc around opposition. The
|
|
144
|
+
* observer body itself is reported at its own heliocentric place. With no
|
|
145
|
+
* observer the positions are heliocentric.
|
|
146
|
+
*
|
|
147
|
+
* @param sys The authored system.
|
|
148
|
+
* @param tDays The instant, in the same day units as each body's `periodDays`
|
|
149
|
+
* and `epoch`.
|
|
150
|
+
* @returns Position per body id (`lonDeg`, `latDeg`, `r`).
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const sys: SyntheticSystem = {
|
|
154
|
+
* id: "twin-moons",
|
|
155
|
+
* bodies: [
|
|
156
|
+
* { id: "fast", mode: "periodic", periodDays: 20, phaseDeg: 0 },
|
|
157
|
+
* { id: "slow", mode: "periodic", periodDays: 80, phaseDeg: 0 },
|
|
158
|
+
* ],
|
|
159
|
+
* };
|
|
160
|
+
* syntheticPositions(sys, 0).fast.lonDeg; // 0
|
|
161
|
+
* syntheticPositions(sys, 5).fast.lonDeg; // 90 (a quarter of its 20-day period)
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function syntheticPositions(sys, tDays) {
|
|
165
|
+
const sources = syntheticSources(sys);
|
|
166
|
+
const obs = sys.observer ? sources[sys.observer]?.xyz(tDays) ?? null : null;
|
|
167
|
+
const out = {};
|
|
168
|
+
for (const b of sys.bodies) {
|
|
169
|
+
const v = sources[b.id].xyz(tDays);
|
|
170
|
+
if (obs && sys.observer !== b.id) {
|
|
171
|
+
out[b.id] = vecToPos([v[0] - obs[0], v[1] - obs[1], v[2] - obs[2]]);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
out[b.id] = vecToPos(v);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A {@link BodyPositionSource} over an authored system: the world-frame view as
|
|
181
|
+
* a reusable object, with longitude speed and a retrograde flag so it satisfies
|
|
182
|
+
* the same contract real ephemeris bodies do. Carries the system's
|
|
183
|
+
* {@link validateSyntheticSystem} diagnosis (`impossible`/`problems`) — the
|
|
184
|
+
* honesty pattern — and still serves the bodies that are individually valid.
|
|
185
|
+
*
|
|
186
|
+
* @param sys The authored system.
|
|
187
|
+
* @returns A position source: `bodies()`, `longitude(id, t)`, `position(id, t)`,
|
|
188
|
+
* plus `impossible`/`problems`.
|
|
189
|
+
*/
|
|
190
|
+
export function syntheticEphemeris(sys) {
|
|
191
|
+
const { impossible, problems } = validateSyntheticSystem(sys);
|
|
192
|
+
const sources = syntheticSources(sys);
|
|
193
|
+
const ids = sys.bodies.map((b) => b.id);
|
|
194
|
+
const apparent = (id, t) => {
|
|
195
|
+
const v = sources[id].xyz(t);
|
|
196
|
+
if (sys.observer && sys.observer !== id) {
|
|
197
|
+
const o = sources[sys.observer].xyz(t);
|
|
198
|
+
return vecToPos([v[0] - o[0], v[1] - o[1], v[2] - o[2]]);
|
|
199
|
+
}
|
|
200
|
+
return vecToPos(v);
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
impossible, problems,
|
|
204
|
+
bodies: () => [...ids],
|
|
205
|
+
longitude: (id, t) => apparent(id, t).lonDeg,
|
|
206
|
+
position: (id, t) => {
|
|
207
|
+
const p = apparent(id, t);
|
|
208
|
+
const h = 0.05; // days; central difference, matching Engine.position
|
|
209
|
+
const l0 = apparent(id, t - h).lonDeg;
|
|
210
|
+
const l1 = apparent(id, t + h).lonDeg;
|
|
211
|
+
const speed = (mod(l1 - l0 + 540, 360) - 180) / (2 * h);
|
|
212
|
+
return { ...p, speed, retrograde: speed < 0 };
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Register a system's bodies on a real {@link Engine} so `transits`, `returns`,
|
|
218
|
+
* retrograde detection, `skyView`, `relational` — every computation built on
|
|
219
|
+
* `Engine.position`/`Engine.longitude` — consume them unchanged. The bodies are
|
|
220
|
+
* placed heliocentrically in the real solar system and viewed from real Earth,
|
|
221
|
+
* so the synthetic `observer` is *not* applied here (use
|
|
222
|
+
* {@link syntheticPositions} for the self-contained world frame). Each body's
|
|
223
|
+
* `epoch` is then interpreted as a TT Julian Day.
|
|
224
|
+
*
|
|
225
|
+
* @param engine The engine to register onto (anything with `registerSource`).
|
|
226
|
+
* @param sys The authored system.
|
|
227
|
+
* @returns The ids registered.
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* registerSyntheticSystem(engine, {
|
|
231
|
+
* id: "nemesis", bodies: [{ id: "nemesis", mode: "kepler", a: 520, e: 0.1,
|
|
232
|
+
* i: 5, node: 0, peri: 0, M0: 0, periodDays: 4_300_000, epoch: 2451545 }],
|
|
233
|
+
* });
|
|
234
|
+
* returns(engine, "nemesis", natalJd, jdStart, jdEnd); // works, zero changes
|
|
235
|
+
* engine.position("nemesis", jd).retrograde; // apparent, from Earth
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export function registerSyntheticSystem(engine, sys) {
|
|
239
|
+
const sources = syntheticSources(sys);
|
|
240
|
+
for (const b of sys.bodies) {
|
|
241
|
+
engine.registerSource(b.id, sources[b.id]);
|
|
242
|
+
const render = sys.render?.[b.id];
|
|
243
|
+
if (render)
|
|
244
|
+
engine.registerRender?.(b.id, render);
|
|
245
|
+
}
|
|
246
|
+
return sys.bodies.map((b) => b.id);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The {@link SyntheticRender} attributes for a body id, or `undefined` when none
|
|
250
|
+
* were authored. The accessor SkyView/`skyViewSequence` reads so an imaginary
|
|
251
|
+
* body keeps a consistent size, brightness and colour across frames — the engine
|
|
252
|
+
* owns position, this owns appearance.
|
|
253
|
+
*
|
|
254
|
+
* @param sys The authored system.
|
|
255
|
+
* @param id A body id.
|
|
256
|
+
* @returns The render attributes, or `undefined`.
|
|
257
|
+
*/
|
|
258
|
+
export function syntheticRender(sys, id) {
|
|
259
|
+
return sys.render?.[id];
|
|
260
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caelus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "Clean-room TypeScript astrology computation: ephemeris, charts, events, timing techniques, Vedic methods, and citable chart facts. MIT, no AGPL or ephemeris files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
7
7
|
"types": "dist/src/index.d.ts",
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
"data/pluto_meeus37.json",
|
|
16
16
|
"data/chiron_cheb.json",
|
|
17
17
|
"data/moon_cheb.embedded.json",
|
|
18
|
-
"data/fixed_stars.json"
|
|
18
|
+
"data/fixed_stars.json",
|
|
19
|
+
"data/fixed_stars_deep.json",
|
|
20
|
+
"data/constellations.json"
|
|
19
21
|
],
|
|
20
22
|
"scripts": {
|
|
21
23
|
"build": "tsc",
|
|
22
|
-
"test": "node dist/test/golden.test.js"
|
|
24
|
+
"test": "node dist/test/golden.test.js && node dist/test/skyview.test.js && node dist/test/synthetic.test.js"
|
|
23
25
|
},
|
|
24
26
|
"license": "MIT",
|
|
25
27
|
"publishConfig": {
|