@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.
- package/README.md +20 -0
- package/package.json +6 -2
- package/src/astro.js +400 -277
- package/src/ballisticPropagator.js +1 -1
- package/src/index.js +3 -0
- package/src/wasmProp/index.js +10 -0
- package/src/wasmProp/primitives.js +295 -0
- package/src/wasmProp/runtime.js +147 -0
- package/src/wasmProp/wasmAstro.js +251 -0
|
@@ -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
|
+
};
|