@saber-usa/node-common 1.7.17 → 1.7.18-alpha.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.
@@ -0,0 +1,251 @@
1
+ import {
2
+ EciBaseCalculator,
3
+ GmstCalculator,
4
+ GeodeticPositionCalculator,
5
+ } from "satellite.js";
6
+ import {getBulkRuntime, getOrCreateBulkPropagator} from "./runtime.js";
7
+ import {elsetsToSatrecs, propBulk} from "./primitives.js";
8
+ import {
9
+ assembleLeoRpoResult,
10
+ assembleGeoRpoResult,
11
+ buildShadowZoneElsets,
12
+ analyzeShadowZoneSeries,
13
+ buildWaterfallBreakpoints,
14
+ decorateWaterfallSegment,
15
+ assembleWaterfallRows,
16
+ checkTle,
17
+ getLonAndDrift,
18
+ getEclipseStatus,
19
+ } from "../astro.js";
20
+ import {isDefined} from "../utils.js";
21
+ import {RAD2DEG} from "../constants.js";
22
+
23
+ // ----------------------------------------------------------------------------
24
+ // High-level WASM-backed bulk siblings of the `getLeoRpoData`,
25
+ // `getGeoRpoData`, `getLeoWaterfallData`, and `getGeoShadowZones` entry
26
+ // points. Each function shares its pure assembly helpers with the JS-path
27
+ // sibling in `../astro.js` to prevent logic drift between the two paths.
28
+ //
29
+ // See `docs/BULK_PROPAGATION.md` for the design rationale, the rollout
30
+ // table, and the measured break-even threshold that drives flip decisions.
31
+ // ----------------------------------------------------------------------------
32
+
33
+ /**
34
+ * WASM-backed sibling of `getLeoRpoData`. Computes ephemerides for
35
+ * `[primary, ...sats]` in a single `propBulk` call (one WASM round-trip,
36
+ * one allocation pass, identical 10-second step), then forwards each
37
+ * (primary, threat) pair through the same `assembleLeoRpoResult` helper
38
+ * used by the JS path.
39
+ *
40
+ * Same contract as `getLeoRpoData` — same input shape, same output shape —
41
+ * but `async` because the WASM runtime is initialized lazily.
42
+ *
43
+ * Use when `sats.length × dates` is high enough to clear the break-even
44
+ * threshold documented in `docs/BULK_PROPAGATION.md` §10. For small
45
+ * `sats.length` (e.g. one or two threats) prefer the sync `getLeoRpoData`.
46
+ *
47
+ * @param {String} line1 line 1 of the target satellite
48
+ * @param {String} line2 line 2 of the target satellite
49
+ * @param {Array<Object>} sats array of potential threat satellites and their Elsets
50
+ * @param {Number} startTime start time of the analysis, Unix milliseconds
51
+ * @param {Number} endTime end time of the analysis, Unix milliseconds
52
+ * @return {Promise<Array<Object>>}
53
+ */
54
+ const getLeoRpoDataBulk = async (line1, line2, sats, startTime, endTime) => {
55
+ const results = [];
56
+ const pSatRec = checkTle(line1, line2);
57
+ if (!isDefined(pSatRec)) {return results;}
58
+
59
+ const start = new Date(startTime).getTime();
60
+ const end = new Date(endTime).getTime();
61
+ const pElset = {Line1: line1, Line2: line2};
62
+
63
+ // One WASM round-trip for [primary, ...threats]. propBulk preserves
64
+ // input order in its returned outer array.
65
+ const ephemerides = await propBulk([pElset, ...sats], start, end, 10000);
66
+ const pEphem = ephemerides[0];
67
+ if (!isDefined(pEphem) || pEphem.length === 0) {
68
+ return results; // Primary may have re-entered the atmosphere.
69
+ }
70
+
71
+ for (let i = 0; i < sats.length; i++) {
72
+ const row = assembleLeoRpoResult(sats[i], pEphem, ephemerides[i + 1]);
73
+ if (isDefined(row)) {results.push(row);}
74
+ }
75
+ return results;
76
+ };
77
+
78
+ /**
79
+ * WASM-backed sibling of `getGeoRpoData`. Computes ephemerides for
80
+ * `[primary, ...sats]` in a single `propBulk` call (60-second step,
81
+ * matching the JS path), then assembles each (primary, threat) pair
82
+ * through the shared `assembleGeoRpoResult` helper.
83
+ *
84
+ * `getLonAndDrift` stays on the JS path: it is a single-satellite
85
+ * Brouwer-mean-element calculation that does not benefit from
86
+ * `BulkPropagator`'s amortized batch model (see `docs/BULK_PROPAGATION.md`
87
+ * "low-value opportunities").
88
+ *
89
+ * Same input/output contract as `getGeoRpoData`, but `async` because the
90
+ * WASM runtime is initialized lazily.
91
+ *
92
+ * @param {String} line1 line 1 of the target satellite
93
+ * @param {String} line2 line 2 of the target satellite
94
+ * @param {Array<Object>} sats array of potential threat satellites and their Elsets
95
+ * @param {Number} startTime start time of the analysis, Unix milliseconds
96
+ * @param {Number} endTime end time of the analysis, Unix milliseconds
97
+ * @param {Number} [lonTime] datetime to analyze longitude and lon drift at, Unix ms
98
+ * @return {Promise<Array<Object>>}
99
+ */
100
+ const getGeoRpoDataBulk = async (line1, line2, sats, startTime, endTime, lonTime) => {
101
+ const results = [];
102
+ const start = new Date(startTime).getTime();
103
+ const end = new Date(endTime).getTime();
104
+ const pElset = {Line1: line1, Line2: line2};
105
+
106
+ const ephemerides = await propBulk([pElset, ...sats], start, end, 60000);
107
+ const pEphem = ephemerides[0];
108
+ if (!isDefined(pEphem) || pEphem.length === 0) {
109
+ return results; // Primary may have re-entered the atmosphere
110
+ }
111
+
112
+ const lonEvalTime = lonTime ? new Date(lonTime) : new Date(end);
113
+ const pLonAndDrift = getLonAndDrift(line1, line2, lonEvalTime);
114
+
115
+ for (let i = 0; i < sats.length; i++) {
116
+ const s = sats[i];
117
+ const sLonAndDrift = getLonAndDrift(s.Line1, s.Line2, lonEvalTime);
118
+ const row = assembleGeoRpoResult(s, pEphem, ephemerides[i + 1],
119
+ pLonAndDrift, sLonAndDrift);
120
+ if (isDefined(row)) {results.push(row);}
121
+ }
122
+
123
+ return results;
124
+ };
125
+
126
+ /**
127
+ * WASM-backed sibling of `getGeoShadowZones`. Synthesizes the
128
+ * `360 / accuracySecondsDeg` mean-anomaly GEO satrecs up front and
129
+ * dispatches one bulk propagation for the single requested time. The
130
+ * pipeline `[Eci, Gmst, Geodetic]` is run once so we get both ECI
131
+ * positions (for `getEclipseStatus`) and geodetic longitude (for the
132
+ * zone seam) without a second round-trip.
133
+ *
134
+ * Reuses the same per-thread `eci+gmst+geo` propagator as
135
+ * `propGeodeticBulk` (see `runtime.js`'s pipeline registry), so the
136
+ * WASM compile and pipeline-buffer allocations are amortized across all
137
+ * shadow-zone callers in the same worker.
138
+ *
139
+ * Same input/output contract as `getGeoShadowZones`, but `async`.
140
+ *
141
+ * @param {Date} time
142
+ * @param {Number} [accuracySecondsDeg=0.00416*100]
143
+ * @return {Promise<{penStartWestLon:?number, penStartEastLon:?number}>}
144
+ */
145
+ const getGeoShadowZonesBulk = async (time, accuracySecondsDeg = 0.00416 * 100) => {
146
+ const elsets = buildShadowZoneElsets(accuracySecondsDeg);
147
+ const satrecs = elsetsToSatrecs(elsets);
148
+ const dates = [time instanceof Date ? time : new Date(time)];
149
+
150
+ const runtime = await getBulkRuntime();
151
+ const propagator = getOrCreateBulkPropagator({
152
+ runtime,
153
+ key: "eci+gmst+geo",
154
+ makeCalculators: () => [
155
+ new EciBaseCalculator(),
156
+ new GmstCalculator(),
157
+ new GeodeticPositionCalculator(),
158
+ ],
159
+ satRecsCount: satrecs.length,
160
+ datesCount: dates.length,
161
+ });
162
+ propagator.setSatRecs(satrecs);
163
+ propagator.setDates(dates);
164
+ propagator.run();
165
+
166
+ const {eci, geodeticPosition} = propagator.getRawOutput();
167
+ // raw eci.position layout: [sat0_date0_x, _y, _z, sat0_date1_x, ...]
168
+ // raw geodeticPosition layout: see `propGeodeticBulk` — the
169
+ // satellite.js@7.0.0 lat/lon swap applies here too.
170
+ const res = new Array(satrecs.length);
171
+ for (let s = 0; s < satrecs.length; s++) {
172
+ if (eci.error[s] !== 0) {
173
+ // Defensive: the canonical TLE is expected to propagate cleanly.
174
+ res[s] = {ecl: "SUN", geolon: 0};
175
+ continue;
176
+ }
177
+ const pIdx = s * 3;
178
+ const px = eci.position[pIdx];
179
+ const py = eci.position[pIdx + 1];
180
+ const pz = eci.position[pIdx + 2];
181
+ const gIdx = s * 3;
182
+ res[s] = {
183
+ ecl: getEclipseStatus(dates[0], [px, py, pz]),
184
+ geolon: geodeticPosition[gIdx] * RAD2DEG, // physical lon (see swap note)
185
+ };
186
+ }
187
+ return analyzeShadowZoneSeries(res);
188
+ };
189
+
190
+ /**
191
+ * WASM-backed sibling of `getLeoWaterfallData`. For each segment in
192
+ * the waterfall breakpoint schedule, dispatches a single `propBulk` call
193
+ * over the satellites' currently-active elsets, then forwards each
194
+ * per-segment ephemeris through the shared `decorateWaterfallSegment`
195
+ * helper. The cross-satellite assembly step (`assembleWaterfallRows`) is
196
+ * identical to the JS path.
197
+ *
198
+ * Calls `propBulk` once per segment (segments correspond to elset epoch
199
+ * boundaries — typically 1–10 in a multi-day window) rather than once per
200
+ * (segment × satellite) pair, so the WASM amortization is meaningful even
201
+ * at modest satellite counts. The WASM runtime + propagator are reused
202
+ * across segments via the per-thread registry in `./runtime.js`.
203
+ *
204
+ * Same input/output contract as `getLeoWaterfallData`, but `async`.
205
+ *
206
+ * @param {Array<Array<Object>>} elsets per-satellite elset arrays; primary at index 0
207
+ * @param {Number} startTime Unix ms
208
+ * @param {Number} endTime Unix ms
209
+ * @param {Number} [stepMs=10000] step in milliseconds
210
+ * @return {Promise<Array<Object>>}
211
+ */
212
+ const getLeoWaterfallDataBulk = async (elsets, startTime, endTime, stepMs = 10000) => {
213
+ const start = new Date(startTime).getTime();
214
+ const end = new Date(endTime).getTime();
215
+
216
+ const breakpoints = buildWaterfallBreakpoints(elsets, start, end);
217
+ const currentIndices = elsets.map(() => 0);
218
+ const satEphems = elsets.map(() => []);
219
+
220
+ for (let i = 0; i < breakpoints.length - 1; i++) {
221
+ const bkpoint = breakpoints[i];
222
+ const segmentStart = bkpoint.time;
223
+ const segmentEnd = breakpoints[i + 1].time;
224
+
225
+ if (bkpoint.satIndex >= 0) {
226
+ currentIndices[bkpoint.satIndex] = bkpoint.elsetIndex;
227
+ }
228
+
229
+ const activeElsets = currentIndices.map(
230
+ (elsetIndex, satIndex) => elsets[satIndex][elsetIndex],
231
+ );
232
+ // One WASM round-trip per segment, ordered by satIndex.
233
+
234
+ const segmentEphems = await propBulk(activeElsets, segmentStart, segmentEnd, stepMs);
235
+
236
+ activeElsets.forEach((elset, satIndex) => {
237
+ satEphems[satIndex].push(
238
+ ...decorateWaterfallSegment(segmentEphems[satIndex], elset, bkpoint, satIndex),
239
+ );
240
+ });
241
+ }
242
+
243
+ return assembleWaterfallRows(satEphems);
244
+ };
245
+
246
+ export {
247
+ getLeoRpoDataBulk,
248
+ getGeoRpoDataBulk,
249
+ getGeoShadowZonesBulk,
250
+ getLeoWaterfallDataBulk,
251
+ };