@openwaters/almanac 0.3.0 → 0.4.1
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 +28 -23
- package/dist/eclipse.d.ts +52 -0
- package/dist/eclipse.js +18 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/moon.js +2172 -119
- package/dist/solar.d.ts +66 -0
- package/dist/solar.js +344 -0
- package/dist/transforms.d.ts +9 -0
- package/dist/transforms.js +11 -0
- package/package.json +41 -7
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# Almanac
|
|
2
2
|
|
|
3
|
-
Offline
|
|
4
|
-
rise/set/twilight, moon phase, lunar eclipses (solar planned), and where any fixed
|
|
5
|
-
star stands from a catalog position, computed from pure geometry with zero network
|
|
6
|
-
and zero runtime data files.
|
|
3
|
+
Offline sky engine for the Salish Sea and everywhere else: Sun and Moon positions, rise, set, twilight, Moon phase, lunar eclipses, solar eclipses for an observer, and fixed-star altitude and azimuth from catalog positions, computed from pure geometry with zero network and zero runtime data files.
|
|
7
4
|
|
|
8
5
|
Twin implementations, one behavior:
|
|
9
6
|
|
|
@@ -12,22 +9,22 @@ Twin implementations, one behavior:
|
|
|
12
9
|
- `fixtures/` — the shared test corpus (JPL Horizons, USNO, Espenak) both suites
|
|
13
10
|
must pass; the contract that keeps the ports identical
|
|
14
11
|
|
|
15
|
-
Supported interval: 1950-01-01T00:00Z ≤ t < 2101-01-01T00:00Z; results outside it
|
|
16
|
-
raise a typed error. All instants are UT1-accurate, not civil-UTC-accurate in the far
|
|
17
|
-
future — see the design spec's Conventions section for what that means and why.
|
|
12
|
+
Supported interval: 1950-01-01T00:00Z ≤ t < 2101-01-01T00:00Z; results outside it raise a typed error. All instants are UT1-accurate, while unknown future DUT1 is outside the civil-UTC accuracy promise. See the [public contract](docs/CONTRACT.md#time-and-supported-interval) for the time model.
|
|
18
13
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
14
|
+
- Contract: [`docs/CONTRACT.md`](docs/CONTRACT.md), including coordinates, public behavior, accuracy, and fixture evidence.
|
|
15
|
+
- Scope: [`docs/ROADMAP.md`](docs/ROADMAP.md), including supported behavior and deliberate boundaries.
|
|
16
|
+
- Development and releases: [`CONTRIBUTING.md`](CONTRIBUTING.md), including pinned tools and required checks.
|
|
17
|
+
- Landing page: [openwaters.io/sky](https://openwaters.io/sky), with the library running live in a browser.
|
|
22
18
|
|
|
23
19
|
Algorithms translated from [Astronomy Engine](https://github.com/cosinekitty/astronomy)
|
|
24
20
|
(MIT, Don Cross) — see [NOTICE](NOTICE). MIT licensed.
|
|
25
21
|
|
|
26
22
|
## Performance
|
|
27
23
|
|
|
28
|
-
Almanac includes a shared performance harness for both ports: **
|
|
24
|
+
Almanac includes a shared performance harness for both ports: **22 workloads**
|
|
29
25
|
cover positions, a 228-hour sky track, short/year/polar event windows, full-range
|
|
30
|
-
moon phases,
|
|
26
|
+
moon phases, next/previous/range lunar and solar eclipse searches, including empty
|
|
27
|
+
windows, and solar obscuration over a 228-hour track.
|
|
31
28
|
|
|
32
29
|
The v0.2.1 optimizations reuse Moon and shadow geometry, flatten scratch arrays,
|
|
33
30
|
and refine altitude crossings with fewer position evaluations. In the
|
|
@@ -43,20 +40,18 @@ median query time fell relative to the v0.2.0 astronomy code:
|
|
|
43
40
|
| Previous lunar eclipse | 26.0% | 36.6% |
|
|
44
41
|
| Lunar eclipses / 1950–2100 | 26.4% | 23.4% |
|
|
45
42
|
|
|
46
|
-
TypeScript was measured on Ubuntu
|
|
47
|
-
macOS 15. Each comparison builds both revisions with the same harness and
|
|
48
|
-
toolchain, then takes seven interleaved process pairs with 300 ms warmup per
|
|
49
|
-
process. Build and startup time are excluded. Timings vary by machine; the shared
|
|
50
|
-
correctness fixtures and parity tolerances remain the accuracy gates.
|
|
43
|
+
TypeScript was measured on Ubuntu; Swift used release builds on macOS. Each comparison builds both revisions with the same harness and toolchain, then takes seven interleaved process pairs with 300 ms warmup per process. Build and startup time are excluded. Timings vary by machine; the shared correctness fixtures and parity tolerances remain the accuracy gates.
|
|
51
44
|
|
|
52
|
-
Reproduce the comparison locally from the repository root
|
|
53
|
-
5.9+):
|
|
45
|
+
Reproduce the comparison locally from the repository root with mise 2026.9.1 or newer after installing the configured Node and Swift versions in `mise.toml`:
|
|
54
46
|
|
|
55
47
|
```bash
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
mise install
|
|
49
|
+
mise exec -- npm ci --prefix typescript
|
|
50
|
+
mise exec -- node benchmarks/run.mjs --base v0.2.0 --skip '^solar/'
|
|
58
51
|
```
|
|
59
52
|
|
|
53
|
+
The `--skip` pattern leaves out the solar eclipse workloads, which need a base of 0.4.0 or later.
|
|
54
|
+
|
|
60
55
|
CI runs the harness on code changes and fails on **median regressions over 20%**.
|
|
61
56
|
Results include timing tables, raw samples, checksums, and revision/toolchain
|
|
62
57
|
metadata. See [the harness guide](CONTRIBUTING.md#performance) for choosing a
|
|
@@ -74,7 +69,7 @@ npm install @openwaters/almanac
|
|
|
74
69
|
```
|
|
75
70
|
|
|
76
71
|
```ts
|
|
77
|
-
import { nextLunarEclipse, lunarEclipseVisibility, sunEvents, starAltAz } from '@openwaters/almanac';
|
|
72
|
+
import { nextLunarEclipse, lunarEclipseVisibility, nextSolarEclipse, solarObscuration, sunEvents, starAltAz } from '@openwaters/almanac';
|
|
78
73
|
|
|
79
74
|
const observer = { latitudeDeg: 48.5, longitudeDeg: -123.0 };
|
|
80
75
|
|
|
@@ -82,6 +77,10 @@ const eclipse = nextLunarEclipse(new Date());
|
|
|
82
77
|
const visibility = lunarEclipseVisibility(eclipse, observer);
|
|
83
78
|
console.log(eclipse.kind, eclipse.peak, visibility.visibleAtPeak);
|
|
84
79
|
|
|
80
|
+
const solar = nextSolarEclipse(new Date(), observer);
|
|
81
|
+
console.log(solar.kind, solar.peak, solar.obscuration, solar.sunAltDeg.peak);
|
|
82
|
+
console.log(solarObscuration(solar.peak, observer)); // fraction of the Sun's disc covered, 0 to 1
|
|
83
|
+
|
|
85
84
|
const today = new Date();
|
|
86
85
|
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
|
87
86
|
for (const { kind, time } of sunEvents(today, tomorrow, observer)) {
|
|
@@ -95,7 +94,7 @@ const { azDeg, altDeg } = starAltAz(88.792939, 7.407064, today, observer);
|
|
|
95
94
|
### Swift
|
|
96
95
|
|
|
97
96
|
```swift
|
|
98
|
-
.package(url: "https://github.com/openwatersio/almanac.git", exact: "0.
|
|
97
|
+
.package(url: "https://github.com/openwatersio/almanac.git", exact: "0.4.0")
|
|
99
98
|
```
|
|
100
99
|
|
|
101
100
|
```swift
|
|
@@ -108,6 +107,10 @@ let eclipse = try nextLunarEclipse(after: Date())
|
|
|
108
107
|
let visibility = try lunarEclipseVisibility(eclipse, observer: observer)
|
|
109
108
|
print(eclipse.kind, eclipse.peak, visibility.visibleAtPeak)
|
|
110
109
|
|
|
110
|
+
let solar = try nextSolarEclipse(after: Date(), observer: observer)
|
|
111
|
+
print(solar.kind, solar.peak, solar.obscuration, solar.sunAltDeg.peak)
|
|
112
|
+
print(try solarObscuration(at: solar.peak, observer: observer)) // fraction of the Sun's disc covered, 0 to 1
|
|
113
|
+
|
|
111
114
|
let today = Date()
|
|
112
115
|
let tomorrow = today.addingTimeInterval(24 * 60 * 60)
|
|
113
116
|
for event in try sunEvents(from: today, to: tomorrow, observer: observer) {
|
|
@@ -136,3 +139,5 @@ let eclipses = try lunarEclipses(from: today, to: tomorrow)
|
|
|
136
139
|
Ranges include peaks at the start and exclude peaks at the end. Contacts may
|
|
137
140
|
extend outside the range. Previous/next searches skip peaks within 100 ms of the
|
|
138
141
|
anchor. Search results are global; apply `lunarEclipseVisibility` for an observer.
|
|
142
|
+
|
|
143
|
+
Solar eclipses are searched for an observer, because their contacts only exist for a place: `nextSolarEclipse(after, observer)`, `previousSolarEclipse(before, observer)`, and `solarEclipses(startUtc, endUtc, observer)`. An eclipse whose Sun is below the horizon at C1, the peak, and C4 is not returned.
|
package/dist/eclipse.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Observer } from './types.js';
|
|
2
|
+
import { Vec3 } from './nutation.js';
|
|
2
3
|
/** A lunar eclipse: peak circumstances plus the contact instants around them. */
|
|
3
4
|
export interface LunarEclipse {
|
|
4
5
|
/** `total` if the Moon is fully inside the umbra, `partial` if it touches
|
|
@@ -39,6 +40,57 @@ export interface LunarEclipseVisibility {
|
|
|
39
40
|
p4: boolean;
|
|
40
41
|
};
|
|
41
42
|
}
|
|
43
|
+
/** UPSTREAM: `PruneLatitude`, inside `SearchLunarEclipse` — full- or new-Moon
|
|
44
|
+
* ecliptic latitude above which no eclipse is possible.
|
|
45
|
+
* INTERNAL: shared with solar.ts. */
|
|
46
|
+
export declare const PRUNE_LATITUDE_DEG = 1.8;
|
|
47
|
+
/** Root-finder tolerance for every shadow search, seconds — upstream's `Search` default.
|
|
48
|
+
* INTERNAL: shared with solar.ts. */
|
|
49
|
+
export declare const SHADOW_TOL_SECONDS = 1;
|
|
50
|
+
/** UPSTREAM: `Search`'s default `iter_limit`.
|
|
51
|
+
* INTERNAL: shared with solar.ts. */
|
|
52
|
+
export declare const SHADOW_ITER_CAP = 20;
|
|
53
|
+
/**
|
|
54
|
+
* Peaks within 100 ms of a next/previous search anchor count as the same
|
|
55
|
+
* eclipse. The fixed full-moon seed below removes within-runtime drift; keep
|
|
56
|
+
* the band for peaks saved by older versions or computed on another runtime.
|
|
57
|
+
*
|
|
58
|
+
* This can skip a wanted peak when the anchor is within 100 ms of it, but
|
|
59
|
+
* cannot skip a distinct eclipse (the catalog's minimum gap is 29 days).
|
|
60
|
+
* Range searches use exact half-open bounds without this band.
|
|
61
|
+
* INTERNAL: shared with solar.ts.
|
|
62
|
+
*/
|
|
63
|
+
export declare const SAME_ECLIPSE_MS = 100;
|
|
64
|
+
/**
|
|
65
|
+
* UPSTREAM: `ShadowInfo` (astronomy.ts ~8445). `target` and `dir` are the
|
|
66
|
+
* inputs `CalcShadow` measured from: the lunar case passes the geocentric
|
|
67
|
+
* Moon against the Sun-to-Earth line, the solar case (solar.ts) the
|
|
68
|
+
* lunacentric observer against the heliocentric Moon, and the obscuration
|
|
69
|
+
* path reads them back. Upstream's `time` is an `AstroTime`; here the instant
|
|
70
|
+
* travels as days since J2000 UT, as everywhere in L3.
|
|
71
|
+
* INTERNAL: exported for `solar.ts`, not part of the public API.
|
|
72
|
+
*/
|
|
73
|
+
export interface ShadowInfo {
|
|
74
|
+
/** Days since J2000 (UT). */
|
|
75
|
+
ut: number;
|
|
76
|
+
/** Shadow-axis parameter: distance to the shadow plane over the casting body's distance. */
|
|
77
|
+
u: number;
|
|
78
|
+
/** Distance from `target` to the shadow axis, km. */
|
|
79
|
+
r: number;
|
|
80
|
+
/** Umbra radius at the shadow plane, km. */
|
|
81
|
+
k: number;
|
|
82
|
+
/** Penumbra radius at the shadow plane, km. */
|
|
83
|
+
p: number;
|
|
84
|
+
/** The point measured from, AU. */
|
|
85
|
+
target: Vec3;
|
|
86
|
+
/** The shadow axis, AU. */
|
|
87
|
+
dir: Vec3;
|
|
88
|
+
}
|
|
89
|
+
/** UPSTREAM: `CalcShadow`, astronomy.ts ~8458. INTERNAL, shared with solar.ts. */
|
|
90
|
+
export declare function calcShadow(bodyRadiusKm: number, ut: number, target: Vec3, dir: Vec3): ShadowInfo;
|
|
91
|
+
/** UPSTREAM: `MoonEclipticLatitudeDegrees`, astronomy.ts ~8616.
|
|
92
|
+
* INTERNAL: shared with solar.ts. */
|
|
93
|
+
export declare function moonEclipticLatitudeDeg(ut: number): number;
|
|
42
94
|
/**
|
|
43
95
|
* The first lunar eclipse whose peak falls strictly after `after`.
|
|
44
96
|
*
|
package/dist/eclipse.js
CHANGED
|
@@ -28,9 +28,10 @@ import { MOON_MEAN_RADIUS_KM, SUN_RADIUS_KM, search, searchMoonPhase } from './e
|
|
|
28
28
|
* The 88 km of atmosphere is what makes the umbra match observed eclipse
|
|
29
29
|
* magnitudes; it is not the geometric Earth radius. */
|
|
30
30
|
const EARTH_ECLIPSE_RADIUS_KM = 6371.0 + 88.0;
|
|
31
|
-
/** UPSTREAM: `PruneLatitude`, inside `SearchLunarEclipse` — full-Moon
|
|
32
|
-
* latitude above which no eclipse is possible.
|
|
33
|
-
|
|
31
|
+
/** UPSTREAM: `PruneLatitude`, inside `SearchLunarEclipse` — full- or new-Moon
|
|
32
|
+
* ecliptic latitude above which no eclipse is possible.
|
|
33
|
+
* INTERNAL: shared with solar.ts. */
|
|
34
|
+
export const PRUNE_LATITUDE_DEG = 1.8;
|
|
34
35
|
/** Spec: the search gives up after two years. The catalog's longest gap between
|
|
35
36
|
* consecutive lunar eclipses over 1950-2100 is under a year. */
|
|
36
37
|
const SCAN_LIMIT_DAYS = 730;
|
|
@@ -39,10 +40,12 @@ const PEAK_WINDOW_DAYS = 0.03;
|
|
|
39
40
|
/** Upstream's outermost `ShadowSemiDurationMinutes` window, in minutes. */
|
|
40
41
|
const PENUMBRAL_WINDOW_MINUTES = 200.0;
|
|
41
42
|
const MINUTES_PER_DAY = 24.0 * 60.0;
|
|
42
|
-
/** Root-finder tolerance for every shadow search, seconds — upstream's `Search` default.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
/** Root-finder tolerance for every shadow search, seconds — upstream's `Search` default.
|
|
44
|
+
* INTERNAL: shared with solar.ts. */
|
|
45
|
+
export const SHADOW_TOL_SECONDS = 1;
|
|
46
|
+
/** UPSTREAM: `Search`'s default `iter_limit`.
|
|
47
|
+
* INTERNAL: shared with solar.ts. */
|
|
48
|
+
export const SHADOW_ITER_CAP = 20;
|
|
46
49
|
/**
|
|
47
50
|
* Peaks within 100 ms of a next/previous search anchor count as the same
|
|
48
51
|
* eclipse. The fixed full-moon seed below removes within-runtime drift; keep
|
|
@@ -51,10 +54,11 @@ const SHADOW_ITER_CAP = 20;
|
|
|
51
54
|
* This can skip a wanted peak when the anchor is within 100 ms of it, but
|
|
52
55
|
* cannot skip a distinct eclipse (the catalog's minimum gap is 29 days).
|
|
53
56
|
* Range searches use exact half-open bounds without this band.
|
|
57
|
+
* INTERNAL: shared with solar.ts.
|
|
54
58
|
*/
|
|
55
|
-
const SAME_ECLIPSE_MS = 100;
|
|
56
|
-
/** UPSTREAM: `CalcShadow`, astronomy.ts ~8458. */
|
|
57
|
-
function calcShadow(bodyRadiusKm, ut, target, dir) {
|
|
59
|
+
export const SAME_ECLIPSE_MS = 100;
|
|
60
|
+
/** UPSTREAM: `CalcShadow`, astronomy.ts ~8458. INTERNAL, shared with solar.ts. */
|
|
61
|
+
export function calcShadow(bodyRadiusKm, ut, target, dir) {
|
|
58
62
|
const u = (dir.x * target.x + dir.y * target.y + dir.z * target.z) / (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
59
63
|
const dx = (u * dir.x) - target.x;
|
|
60
64
|
const dy = (u * dir.y) - target.y;
|
|
@@ -62,7 +66,7 @@ function calcShadow(bodyRadiusKm, ut, target, dir) {
|
|
|
62
66
|
const r = KM_PER_AU * Math.hypot(dx, dy, dz);
|
|
63
67
|
const k = +SUN_RADIUS_KM - (1.0 + u) * (SUN_RADIUS_KM - bodyRadiusKm);
|
|
64
68
|
const p = -SUN_RADIUS_KM + (1.0 + u) * (SUN_RADIUS_KM + bodyRadiusKm);
|
|
65
|
-
return { ut, u, r, k, p };
|
|
69
|
+
return { ut, u, r, k, p, target, dir };
|
|
66
70
|
}
|
|
67
71
|
/**
|
|
68
72
|
* UPSTREAM: `EarthShadow`, astronomy.ts ~8470 — the Earth's shadow cone
|
|
@@ -119,8 +123,9 @@ function shadowSemiDurationMinutes(shadow, radiusLimitKm, windowMinutes) {
|
|
|
119
123
|
throw new Error('almanac internal: failed to find shadow semiduration');
|
|
120
124
|
return (t2 - t1) * (MINUTES_PER_DAY / 2.0); // convert days to minutes and average the semi-durations
|
|
121
125
|
}
|
|
122
|
-
/** UPSTREAM: `MoonEclipticLatitudeDegrees`, astronomy.ts ~8616.
|
|
123
|
-
|
|
126
|
+
/** UPSTREAM: `MoonEclipticLatitudeDegrees`, astronomy.ts ~8616.
|
|
127
|
+
* INTERNAL: shared with solar.ts. */
|
|
128
|
+
export function moonEclipticLatitudeDeg(ut) {
|
|
124
129
|
return RAD2DEG * calcMoon(ttDaysFromUt(ut)).geoEclipLat;
|
|
125
130
|
}
|
|
126
131
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export type { SunEvent, SunEventKind, MoonEvent, MoonEventKind, MoonPhaseEvent,
|
|
|
10
10
|
export { sunEvents, moonEvents, searchMoonPhases } from './events.js';
|
|
11
11
|
export type { LunarEclipse, LunarEclipseVisibility } from './eclipse.js';
|
|
12
12
|
export { nextLunarEclipse, previousLunarEclipse, lunarEclipses, lunarEclipseVisibility } from './eclipse.js';
|
|
13
|
+
export type { SolarEclipse, SolarEclipseKind, SolarEclipseSunAltitudes } from './solar.js';
|
|
14
|
+
export { nextSolarEclipse, previousSolarEclipse, solarEclipses, solarObscuration } from './solar.js';
|
package/dist/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { sunAltAz, moonAltAz, starAltAz } from './transforms.js';
|
|
|
4
4
|
export { moonIllumination } from './illumination.js';
|
|
5
5
|
export { sunEvents, moonEvents, searchMoonPhases } from './events.js';
|
|
6
6
|
export { nextLunarEclipse, previousLunarEclipse, lunarEclipses, lunarEclipseVisibility } from './eclipse.js';
|
|
7
|
+
export { nextSolarEclipse, previousSolarEclipse, solarEclipses, solarObscuration } from './solar.js';
|