@saber-usa/node-common 1.7.18-alpha.1 → 1.7.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saber-usa/node-common",
3
- "version": "1.7.18-alpha.1",
3
+ "version": "1.7.19",
4
4
  "description": "Common node functions for Saber",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "mathjs": "^15.2.0",
30
30
  "pious-squid": "^2.3.0",
31
31
  "plotly": "^1.0.6",
32
- "satellite.js": "^7.0.0",
32
+ "satellite.js": "^7.0.1",
33
33
  "solar-calculator": "^0.3.0",
34
34
  "three": "^0.184",
35
35
  "winston": "3.19.0"
@@ -1,10 +1,10 @@
1
- // Barrel for all WASM-backed bulk propagation exports. Kept behind a
2
- // single entry point so the rest of the codebase (and external consumers
3
- // via `@saber-usa/node-common`) can discover the bulk API without having
4
- // to reach into per-feature files.
5
- //
6
- // See `docs/BULK_PROPAGATION.md` for the design rationale, lifecycle, and
7
- // the measured performance characteristics.
8
- export * from "./runtime.js";
9
- export * from "./primitives.js";
10
- export * from "./wasmAstro.js";
1
+ // Barrel for all WASM-backed bulk propagation exports. Kept behind a
2
+ // single entry point so the rest of the codebase (and external consumers
3
+ // via `@saber-usa/node-common`) can discover the bulk API without having
4
+ // to reach into per-feature files.
5
+ //
6
+ // See `docs/BULK_PROPAGATION.md` for the design rationale, lifecycle, and
7
+ // the measured performance characteristics.
8
+ export * from "./runtime.js";
9
+ export * from "./primitives.js";
10
+ export * from "./wasmAstro.js";
@@ -1,295 +1,295 @@
1
- import {
2
- twoline2satrec,
3
- EciBaseCalculator,
4
- GmstCalculator,
5
- EcfPositionCalculator,
6
- GeodeticPositionCalculator,
7
- LookAnglesCalculator,
8
- } from "satellite.js";
9
- import {getBulkRuntime, getOrCreateBulkPropagator} from "./runtime.js";
10
- import {RAD2DEG} from "../constants.js";
11
-
12
- // ----------------------------------------------------------------------------
13
- // WASM-backed bulk primitives
14
- //
15
- // Sibling functions to `prop` / `propGeodetic` / the manual LookAngles chain
16
- // that live in `../astro.js`. The originals stay synchronous; these are
17
- // async because the per-thread `BulkPropagator` runtime is async-bootstrapped
18
- // (see `docs/BULK_PROPAGATION.md`).
19
- //
20
- // Time-grid semantics are matched exactly to the JS-path siblings to keep
21
- // parity tests trivial:
22
- // - `propBulk` mirrors `prop` -> half-open [start, end)
23
- // - `propGeodeticBulk` mirrors `propGeodetic` -> closed [start, end]
24
- //
25
- // Return shapes mirror the JS-path siblings element-wise so callers can swap
26
- // transparently. `meanElements` is intentionally absent from `propBulk`
27
- // records: the WASM `EciBaseCalculator` exposes only position/velocity/error,
28
- // it does not surface SGP4 mean elements. Callers that need them must either
29
- // stay on `prop` or recompute osculating elements from p/v.
30
- // ----------------------------------------------------------------------------
31
-
32
- /**
33
- * Builds the closed list of evaluation timestamps for a half-open or closed
34
- * interval, matching the loop semantics of `prop` / `propGeodetic`.
35
- *
36
- * @param {number} start
37
- * @param {number} end
38
- * @param {number} stepMs
39
- * @param {boolean} inclusive when true, end is included (propGeodetic).
40
- * @return {number[]}
41
- */
42
- const buildTimeGrid = (start, end, stepMs, inclusive) => {
43
- const grid = [];
44
- if (inclusive) {
45
- for (let t = start; t <= end; t = t + stepMs) {grid.push(t);}
46
- } else {
47
- for (let t = start; t < end; t = t + stepMs) {grid.push(t);}
48
- }
49
- return grid;
50
- };
51
-
52
- /**
53
- * Bulk-converts an array of `{Line1, Line2}` elsets into satrecs, returning
54
- * `null` if any single elset fails to convert. Mirrors the silent-skip
55
- * behavior of `prop` (which returns `[]` on the first invalid pv).
56
- *
57
- * @param {Array<{Line1: string, Line2: string}>} elsets
58
- * @return {Array|null}
59
- */
60
- const elsetsToSatrecs = (elsets) => {
61
- const satrecs = elsets.map((e) => twoline2satrec(e.Line1, e.Line2));
62
- return satrecs;
63
- };
64
-
65
- /**
66
- * WASM-backed sibling of `prop`. Computes ECI position and velocity for
67
- * `elsets.length` satellites at every step in the half-open interval
68
- * `[start, end)` with step `stepMs`.
69
- *
70
- * Returns `Array<Array<{p, v, t}>>` indexed `[satIdx][dateIdx]`. Each inner
71
- * array matches the shape returned by `prop` minus the `meanElements` field
72
- * (see module-level note). If the WASM SGP4 produces a non-zero error code
73
- * for any (sat, date) pair, that satellite's entire ephemeris is returned as
74
- * `[]` — same all-or-nothing semantic as `prop` on `propTo` failure.
75
- *
76
- * @param {Array<{Line1: string, Line2: string}>} elsets
77
- * @param {number} start UNIX milliseconds
78
- * @param {number} end UNIX milliseconds (exclusive)
79
- * @param {number} stepMs default 1000
80
- * @return {Promise<Array<Array<{p: object, v: object, t: number}>>>}
81
- */
82
- const propBulk = async (elsets, start, end, stepMs = 1000) => {
83
- const grid = buildTimeGrid(start, end, stepMs, false);
84
- if (elsets.length === 0 || grid.length === 0) {
85
- return elsets.map(() => []);
86
- }
87
- const satrecs = elsetsToSatrecs(elsets);
88
- const dates = grid.map((t) => new Date(t));
89
-
90
- const runtime = await getBulkRuntime();
91
- const propagator = getOrCreateBulkPropagator({
92
- runtime,
93
- key: "eci",
94
- makeCalculators: () => [new EciBaseCalculator()],
95
- satRecsCount: satrecs.length,
96
- datesCount: dates.length,
97
- });
98
- propagator.setSatRecs(satrecs);
99
- propagator.setDates(dates);
100
- propagator.run();
101
-
102
- const {eci} = propagator.getRawOutput();
103
- // raw layout: [sat0 date0 (x,y,z), sat0 date1 (x,y,z), ..., sat1 date0, ...]
104
- // error layout: [sat0 date0, sat0 date1, ..., sat1 date0, ...]
105
- const out = new Array(satrecs.length);
106
- const m = dates.length;
107
- for (let s = 0; s < satrecs.length; s++) {
108
- const ephem = new Array(m);
109
- let satFailed = false;
110
- for (let d = 0; d < m; d++) {
111
- const errIdx = s * m + d;
112
- if (eci.error[errIdx] !== 0) {
113
- satFailed = true;
114
- break;
115
- }
116
- const vec = (s * m + d) * 3;
117
- ephem[d] = {
118
- p: {
119
- x: eci.position[vec],
120
- y: eci.position[vec + 1],
121
- z: eci.position[vec + 2],
122
- },
123
- v: {
124
- x: eci.velocity[vec],
125
- y: eci.velocity[vec + 1],
126
- z: eci.velocity[vec + 2],
127
- },
128
- t: grid[d],
129
- };
130
- }
131
- out[s] = satFailed ? [] : ephem;
132
- }
133
- return out;
134
- };
135
-
136
- /**
137
- * WASM-backed sibling of `propGeodetic`. Computes geodetic
138
- * (lat, lon, t) for every satellite at every step in the closed interval
139
- * `[start, end]`.
140
- *
141
- * Lat/Lon are returned in **degrees** to match `propGeodetic`. Returns
142
- * `Array<Array<{lat, lon, t}>>` indexed `[satIdx][dateIdx]`. On any SGP4
143
- * error for a satellite the whole ephemeris for that satellite is `[]`,
144
- * matching `propGeodetic`'s short-circuit behavior.
145
- *
146
- * @param {Array<{Line1: string, Line2: string}>} elsets
147
- * @param {number} start UNIX milliseconds
148
- * @param {number} end UNIX milliseconds (inclusive)
149
- * @param {number} stepMs default 60000
150
- * @return {Promise<Array<Array<{lat: number, lon: number, t: number}>>>}
151
- */
152
- const propGeodeticBulk = async (elsets, start, end, stepMs = 60000) => {
153
- const grid = buildTimeGrid(start, end, stepMs, true);
154
- if (elsets.length === 0 || grid.length === 0) {
155
- return elsets.map(() => []);
156
- }
157
- const satrecs = elsetsToSatrecs(elsets);
158
- const dates = grid.map((t) => new Date(t));
159
-
160
- const runtime = await getBulkRuntime();
161
- const propagator = getOrCreateBulkPropagator({
162
- runtime,
163
- key: "eci+gmst+geo",
164
- makeCalculators: () => [
165
- new EciBaseCalculator(),
166
- new GmstCalculator(),
167
- new GeodeticPositionCalculator(),
168
- ],
169
- satRecsCount: satrecs.length,
170
- datesCount: dates.length,
171
- });
172
- propagator.setSatRecs(satrecs);
173
- propagator.setDates(dates);
174
- propagator.run();
175
-
176
- const {eci, geodeticPosition} = propagator.getRawOutput();
177
- // KNOWN satellite.js@7.0.0 BUG / docs error: the
178
- // `GeodeticPositionCalculator` raw layout is documented as
179
- // `[lat, lon, height, ...]`, and `getFormattedOutput()` exposes fields
180
- // `{latitude: raw[0], longitude: raw[1], height: raw[2]}` — but the
181
- // physical values are SWAPPED. Empirically, against the canonical JS
182
- // `eciToGeodetic` path:
183
- // raw[3*k + 0] -> physical LONGITUDE (in radians)
184
- // raw[3*k + 1] -> physical LATITUDE (in radians)
185
- // raw[3*k + 2] -> height (km)
186
- // We map raw -> {lat, lon} accordingly so `propGeodeticBulk` is exactly
187
- // value-equivalent to `propGeodetic`. If a future satellite.js release
188
- // fixes this upstream, swap the indices back here.
189
- const out = new Array(satrecs.length);
190
- const m = dates.length;
191
- for (let s = 0; s < satrecs.length; s++) {
192
- const positions = new Array(m);
193
- let satFailed = false;
194
- for (let d = 0; d < m; d++) {
195
- const errIdx = s * m + d;
196
- if (eci.error[errIdx] !== 0) {
197
- satFailed = true;
198
- break;
199
- }
200
- const vec = (s * m + d) * 3;
201
- positions[d] = {
202
- lat: geodeticPosition[vec + 1] * RAD2DEG,
203
- lon: geodeticPosition[vec] * RAD2DEG,
204
- t: grid[d],
205
- };
206
- }
207
- out[s] = satFailed ? [] : positions;
208
- }
209
- return out;
210
- };
211
-
212
- /**
213
- * WASM-backed bulk look-angles primitive. Computes (azimuth, elevation,
214
- * rangeSat) for every satellite at every supplied date relative to a single
215
- * observer (Geodetic location, **radians + km** as required by satellite.js
216
- * v7 — `{latitude, longitude, height}`).
217
- *
218
- * Unlike `propBulk` and `propGeodeticBulk` this primitive takes an explicit
219
- * `dates` array (rather than start/end/step) because look-angles consumers
220
- * typically already hold the date list (e.g. observation timestamps in
221
- * `GetResiduals`) and synthesizing a regular grid would re-impose JS-side
222
- * step math the WASM pipeline is meant to avoid.
223
- *
224
- * Returns `Array<Array<{azimuth, elevation, rangeSat, t}>>` indexed
225
- * `[satIdx][dateIdx]`, with angles in **radians** and `rangeSat` in km. On
226
- * any SGP4 error for a satellite that satellite's entire output is `[]`.
227
- *
228
- * @param {Array<{Line1: string, Line2: string}>} elsets
229
- * @param {Date[]|number[]} dates
230
- * @param {{latitude: number, longitude: number, height: number}} observerGd
231
- * Observer position in radians (lat/lon) and km (height).
232
- * @return {Promise<Array<Array<{azimuth: number, elevation: number, rangeSat: number, t: number}>>>}
233
- */
234
- const propLookAnglesBulk = async (elsets, dates, observerGd) => {
235
- if (elsets.length === 0 || dates.length === 0) {
236
- return elsets.map(() => []);
237
- }
238
- const satrecs = elsetsToSatrecs(elsets);
239
- const dateObjs = dates.map((d) => (d instanceof Date ? d : new Date(d)));
240
- const ts = dateObjs.map((d) => d.getTime());
241
-
242
- const runtime = await getBulkRuntime();
243
- const propagator = getOrCreateBulkPropagator({
244
- runtime,
245
- key: "eci+gmst+ecf+lookAngles",
246
- makeCalculators: () => [
247
- new EciBaseCalculator(),
248
- new GmstCalculator(),
249
- new EcfPositionCalculator(),
250
- new LookAnglesCalculator(),
251
- ],
252
- satRecsCount: satrecs.length,
253
- datesCount: dateObjs.length,
254
- });
255
- propagator.setSatRecs(satrecs);
256
- propagator.setDates(dateObjs);
257
- propagator.run({lookAngles: {observer: observerGd}});
258
-
259
- const {eci, lookAngles} = propagator.getRawOutput();
260
- // lookAngles layout: [sat0 date0 (az, el, range), sat0 date1, ...]
261
- const out = new Array(satrecs.length);
262
- const m = dateObjs.length;
263
- for (let s = 0; s < satrecs.length; s++) {
264
- const samples = new Array(m);
265
- let satFailed = false;
266
- for (let d = 0; d < m; d++) {
267
- const errIdx = s * m + d;
268
- if (eci.error[errIdx] !== 0) {
269
- satFailed = true;
270
- break;
271
- }
272
- const vec = (s * m + d) * 3;
273
- samples[d] = {
274
- azimuth: lookAngles[vec],
275
- elevation: lookAngles[vec + 1],
276
- rangeSat: lookAngles[vec + 2],
277
- t: ts[d],
278
- };
279
- }
280
- out[s] = satFailed ? [] : samples;
281
- }
282
- return out;
283
- };
284
-
285
- export {
286
- // `buildTimeGrid` is intentionally NOT exported: it is consumed only by
287
- // `propBulk` / `propGeodeticBulk` inside this module. Keeping it private
288
- // avoids polluting the public `@saber-usa/node-common` surface with a
289
- // helper that exists purely to bridge the JS-path loop semantics to the
290
- // WASM `setDates(Date[])` API.
291
- elsetsToSatrecs, // used by `./wasmAstro.js#getGeoShadowZonesBulk`
292
- propBulk,
293
- propGeodeticBulk,
294
- propLookAnglesBulk,
295
- };
1
+ import {
2
+ twoline2satrec,
3
+ EciBaseCalculator,
4
+ GmstCalculator,
5
+ EcfPositionCalculator,
6
+ GeodeticPositionCalculator,
7
+ LookAnglesCalculator,
8
+ } from "satellite.js";
9
+ import {getBulkRuntime, getOrCreateBulkPropagator} from "./runtime.js";
10
+ import {RAD2DEG} from "../constants.js";
11
+
12
+ // ----------------------------------------------------------------------------
13
+ // WASM-backed bulk primitives
14
+ //
15
+ // Sibling functions to `prop` / `propGeodetic` / the manual LookAngles chain
16
+ // that live in `../astro.js`. The originals stay synchronous; these are
17
+ // async because the per-thread `BulkPropagator` runtime is async-bootstrapped
18
+ // (see `docs/BULK_PROPAGATION.md`).
19
+ //
20
+ // Time-grid semantics are matched exactly to the JS-path siblings to keep
21
+ // parity tests trivial:
22
+ // - `propBulk` mirrors `prop` -> half-open [start, end)
23
+ // - `propGeodeticBulk` mirrors `propGeodetic` -> closed [start, end]
24
+ //
25
+ // Return shapes mirror the JS-path siblings element-wise so callers can swap
26
+ // transparently. `meanElements` is intentionally absent from `propBulk`
27
+ // records: the WASM `EciBaseCalculator` exposes only position/velocity/error,
28
+ // it does not surface SGP4 mean elements. Callers that need them must either
29
+ // stay on `prop` or recompute osculating elements from p/v.
30
+ // ----------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Builds the closed list of evaluation timestamps for a half-open or closed
34
+ * interval, matching the loop semantics of `prop` / `propGeodetic`.
35
+ *
36
+ * @param {number} start
37
+ * @param {number} end
38
+ * @param {number} stepMs
39
+ * @param {boolean} inclusive when true, end is included (propGeodetic).
40
+ * @return {number[]}
41
+ */
42
+ const buildTimeGrid = (start, end, stepMs, inclusive) => {
43
+ const grid = [];
44
+ if (inclusive) {
45
+ for (let t = start; t <= end; t = t + stepMs) {grid.push(t);}
46
+ } else {
47
+ for (let t = start; t < end; t = t + stepMs) {grid.push(t);}
48
+ }
49
+ return grid;
50
+ };
51
+
52
+ /**
53
+ * Bulk-converts an array of `{Line1, Line2}` elsets into satrecs, returning
54
+ * `null` if any single elset fails to convert. Mirrors the silent-skip
55
+ * behavior of `prop` (which returns `[]` on the first invalid pv).
56
+ *
57
+ * @param {Array<{Line1: string, Line2: string}>} elsets
58
+ * @return {Array|null}
59
+ */
60
+ const elsetsToSatrecs = (elsets) => {
61
+ const satrecs = elsets.map((e) => twoline2satrec(e.Line1, e.Line2));
62
+ return satrecs;
63
+ };
64
+
65
+ /**
66
+ * WASM-backed sibling of `prop`. Computes ECI position and velocity for
67
+ * `elsets.length` satellites at every step in the half-open interval
68
+ * `[start, end)` with step `stepMs`.
69
+ *
70
+ * Returns `Array<Array<{p, v, t}>>` indexed `[satIdx][dateIdx]`. Each inner
71
+ * array matches the shape returned by `prop` minus the `meanElements` field
72
+ * (see module-level note). If the WASM SGP4 produces a non-zero error code
73
+ * for any (sat, date) pair, that satellite's entire ephemeris is returned as
74
+ * `[]` — same all-or-nothing semantic as `prop` on `propTo` failure.
75
+ *
76
+ * @param {Array<{Line1: string, Line2: string}>} elsets
77
+ * @param {number} start UNIX milliseconds
78
+ * @param {number} end UNIX milliseconds (exclusive)
79
+ * @param {number} stepMs default 1000
80
+ * @return {Promise<Array<Array<{p: object, v: object, t: number}>>>}
81
+ */
82
+ const propBulk = async (elsets, start, end, stepMs = 1000) => {
83
+ const grid = buildTimeGrid(start, end, stepMs, false);
84
+ if (elsets.length === 0 || grid.length === 0) {
85
+ return elsets.map(() => []);
86
+ }
87
+ const satrecs = elsetsToSatrecs(elsets);
88
+ const dates = grid.map((t) => new Date(t));
89
+
90
+ const runtime = await getBulkRuntime();
91
+ const propagator = getOrCreateBulkPropagator({
92
+ runtime,
93
+ key: "eci",
94
+ makeCalculators: () => [new EciBaseCalculator()],
95
+ satRecsCount: satrecs.length,
96
+ datesCount: dates.length,
97
+ });
98
+ propagator.setSatRecs(satrecs);
99
+ propagator.setDates(dates);
100
+ propagator.run();
101
+
102
+ const {eci} = propagator.getRawOutput();
103
+ // raw layout: [sat0 date0 (x,y,z), sat0 date1 (x,y,z), ..., sat1 date0, ...]
104
+ // error layout: [sat0 date0, sat0 date1, ..., sat1 date0, ...]
105
+ const out = new Array(satrecs.length);
106
+ const m = dates.length;
107
+ for (let s = 0; s < satrecs.length; s++) {
108
+ const ephem = new Array(m);
109
+ let satFailed = false;
110
+ for (let d = 0; d < m; d++) {
111
+ const errIdx = s * m + d;
112
+ if (eci.error[errIdx] !== 0) {
113
+ satFailed = true;
114
+ break;
115
+ }
116
+ const vec = (s * m + d) * 3;
117
+ ephem[d] = {
118
+ p: {
119
+ x: eci.position[vec],
120
+ y: eci.position[vec + 1],
121
+ z: eci.position[vec + 2],
122
+ },
123
+ v: {
124
+ x: eci.velocity[vec],
125
+ y: eci.velocity[vec + 1],
126
+ z: eci.velocity[vec + 2],
127
+ },
128
+ t: grid[d],
129
+ };
130
+ }
131
+ out[s] = satFailed ? [] : ephem;
132
+ }
133
+ return out;
134
+ };
135
+
136
+ /**
137
+ * WASM-backed sibling of `propGeodetic`. Computes geodetic
138
+ * (lat, lon, t) for every satellite at every step in the closed interval
139
+ * `[start, end]`.
140
+ *
141
+ * Lat/Lon are returned in **degrees** to match `propGeodetic`. Returns
142
+ * `Array<Array<{lat, lon, t}>>` indexed `[satIdx][dateIdx]`. On any SGP4
143
+ * error for a satellite the whole ephemeris for that satellite is `[]`,
144
+ * matching `propGeodetic`'s short-circuit behavior.
145
+ *
146
+ * @param {Array<{Line1: string, Line2: string}>} elsets
147
+ * @param {number} start UNIX milliseconds
148
+ * @param {number} end UNIX milliseconds (inclusive)
149
+ * @param {number} stepMs default 60000
150
+ * @return {Promise<Array<Array<{lat: number, lon: number, t: number}>>>}
151
+ */
152
+ const propGeodeticBulk = async (elsets, start, end, stepMs = 60000) => {
153
+ const grid = buildTimeGrid(start, end, stepMs, true);
154
+ if (elsets.length === 0 || grid.length === 0) {
155
+ return elsets.map(() => []);
156
+ }
157
+ const satrecs = elsetsToSatrecs(elsets);
158
+ const dates = grid.map((t) => new Date(t));
159
+
160
+ const runtime = await getBulkRuntime();
161
+ const propagator = getOrCreateBulkPropagator({
162
+ runtime,
163
+ key: "eci+gmst+geo",
164
+ makeCalculators: () => [
165
+ new EciBaseCalculator(),
166
+ new GmstCalculator(),
167
+ new GeodeticPositionCalculator(),
168
+ ],
169
+ satRecsCount: satrecs.length,
170
+ datesCount: dates.length,
171
+ });
172
+ propagator.setSatRecs(satrecs);
173
+ propagator.setDates(dates);
174
+ propagator.run();
175
+
176
+ const {eci, geodeticPosition} = propagator.getRawOutput();
177
+ // KNOWN satellite.js@7.0.0 BUG / docs error: the
178
+ // `GeodeticPositionCalculator` raw layout is documented as
179
+ // `[lat, lon, height, ...]`, and `getFormattedOutput()` exposes fields
180
+ // `{latitude: raw[0], longitude: raw[1], height: raw[2]}` — but the
181
+ // physical values are SWAPPED. Empirically, against the canonical JS
182
+ // `eciToGeodetic` path:
183
+ // raw[3*k + 0] -> physical LONGITUDE (in radians)
184
+ // raw[3*k + 1] -> physical LATITUDE (in radians)
185
+ // raw[3*k + 2] -> height (km)
186
+ // We map raw -> {lat, lon} accordingly so `propGeodeticBulk` is exactly
187
+ // value-equivalent to `propGeodetic`. If a future satellite.js release
188
+ // fixes this upstream, swap the indices back here.
189
+ const out = new Array(satrecs.length);
190
+ const m = dates.length;
191
+ for (let s = 0; s < satrecs.length; s++) {
192
+ const positions = new Array(m);
193
+ let satFailed = false;
194
+ for (let d = 0; d < m; d++) {
195
+ const errIdx = s * m + d;
196
+ if (eci.error[errIdx] !== 0) {
197
+ satFailed = true;
198
+ break;
199
+ }
200
+ const vec = (s * m + d) * 3;
201
+ positions[d] = {
202
+ lat: geodeticPosition[vec + 1] * RAD2DEG,
203
+ lon: geodeticPosition[vec] * RAD2DEG,
204
+ t: grid[d],
205
+ };
206
+ }
207
+ out[s] = satFailed ? [] : positions;
208
+ }
209
+ return out;
210
+ };
211
+
212
+ /**
213
+ * WASM-backed bulk look-angles primitive. Computes (azimuth, elevation,
214
+ * rangeSat) for every satellite at every supplied date relative to a single
215
+ * observer (Geodetic location, **radians + km** as required by satellite.js
216
+ * v7 — `{latitude, longitude, height}`).
217
+ *
218
+ * Unlike `propBulk` and `propGeodeticBulk` this primitive takes an explicit
219
+ * `dates` array (rather than start/end/step) because look-angles consumers
220
+ * typically already hold the date list (e.g. observation timestamps in
221
+ * `GetResiduals`) and synthesizing a regular grid would re-impose JS-side
222
+ * step math the WASM pipeline is meant to avoid.
223
+ *
224
+ * Returns `Array<Array<{azimuth, elevation, rangeSat, t}>>` indexed
225
+ * `[satIdx][dateIdx]`, with angles in **radians** and `rangeSat` in km. On
226
+ * any SGP4 error for a satellite that satellite's entire output is `[]`.
227
+ *
228
+ * @param {Array<{Line1: string, Line2: string}>} elsets
229
+ * @param {Date[]|number[]} dates
230
+ * @param {{latitude: number, longitude: number, height: number}} observerGd
231
+ * Observer position in radians (lat/lon) and km (height).
232
+ * @return {Promise<Array<Array<{azimuth: number, elevation: number, rangeSat: number, t: number}>>>}
233
+ */
234
+ const propLookAnglesBulk = async (elsets, dates, observerGd) => {
235
+ if (elsets.length === 0 || dates.length === 0) {
236
+ return elsets.map(() => []);
237
+ }
238
+ const satrecs = elsetsToSatrecs(elsets);
239
+ const dateObjs = dates.map((d) => (d instanceof Date ? d : new Date(d)));
240
+ const ts = dateObjs.map((d) => d.getTime());
241
+
242
+ const runtime = await getBulkRuntime();
243
+ const propagator = getOrCreateBulkPropagator({
244
+ runtime,
245
+ key: "eci+gmst+ecf+lookAngles",
246
+ makeCalculators: () => [
247
+ new EciBaseCalculator(),
248
+ new GmstCalculator(),
249
+ new EcfPositionCalculator(),
250
+ new LookAnglesCalculator(),
251
+ ],
252
+ satRecsCount: satrecs.length,
253
+ datesCount: dateObjs.length,
254
+ });
255
+ propagator.setSatRecs(satrecs);
256
+ propagator.setDates(dateObjs);
257
+ propagator.run({lookAngles: {observer: observerGd}});
258
+
259
+ const {eci, lookAngles} = propagator.getRawOutput();
260
+ // lookAngles layout: [sat0 date0 (az, el, range), sat0 date1, ...]
261
+ const out = new Array(satrecs.length);
262
+ const m = dateObjs.length;
263
+ for (let s = 0; s < satrecs.length; s++) {
264
+ const samples = new Array(m);
265
+ let satFailed = false;
266
+ for (let d = 0; d < m; d++) {
267
+ const errIdx = s * m + d;
268
+ if (eci.error[errIdx] !== 0) {
269
+ satFailed = true;
270
+ break;
271
+ }
272
+ const vec = (s * m + d) * 3;
273
+ samples[d] = {
274
+ azimuth: lookAngles[vec],
275
+ elevation: lookAngles[vec + 1],
276
+ rangeSat: lookAngles[vec + 2],
277
+ t: ts[d],
278
+ };
279
+ }
280
+ out[s] = satFailed ? [] : samples;
281
+ }
282
+ return out;
283
+ };
284
+
285
+ export {
286
+ // `buildTimeGrid` is intentionally NOT exported: it is consumed only by
287
+ // `propBulk` / `propGeodeticBulk` inside this module. Keeping it private
288
+ // avoids polluting the public `@saber-usa/node-common` surface with a
289
+ // helper that exists purely to bridge the JS-path loop semantics to the
290
+ // WASM `setDates(Date[])` API.
291
+ elsetsToSatrecs, // used by `./wasmAstro.js#getGeoShadowZonesBulk`
292
+ propBulk,
293
+ propGeodeticBulk,
294
+ propLookAnglesBulk,
295
+ };