@saber-usa/node-common 1.7.2 → 1.7.3

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/src/OrbitUtils.js CHANGED
@@ -1,490 +1,490 @@
1
- import {MU} from "./constants.js";
2
- import {cross, norm} from "mathjs";
3
- import {wrapHalfRevUnsigned, wrapOneRevUnsigned} from "./utils.js";
4
-
5
- const EARTH_MU_KM = MU / 1e9;
6
-
7
- class OrbitUtils {
8
- constructor() {
9
- // Prevent instantiation
10
- }
11
- /**
12
- * Get inclination based on azimuth and latitude.
13
- * @param {number} azimuthRad - Azimuth in radians
14
- * @param {number} latitudeRad - Latitude in radians
15
- * @return {number} Inclination in radians
16
- */
17
- static getInclination(azimuthRad, latitudeRad) {
18
- const inclination = Math.acos(Math.cos(latitudeRad) * Math.sin(azimuthRad));
19
- return wrapHalfRevUnsigned(inclination);
20
- }
21
-
22
- /**
23
- * Get the azimuth based on inclination and latitude.
24
- * Two results will be possible, based on whether you are Ascending from South to North or Descending from North to South.
25
- * @param {number} inclinationRad - Inclination in radians
26
- * @param {number} latitudeRad - Latitude in radians
27
- * @param {boolean} ascending - Ascending (true) or descending (false)
28
- * @return {number} Azimuth in radians
29
- */
30
- static getAzimuth(inclinationRad, latitudeRad, ascending = true) {
31
- const azimuth = Math.asin(Math.cos(inclinationRad) / Math.cos(latitudeRad));
32
- if (!ascending) {
33
- return wrapOneRevUnsigned(Math.PI - azimuth);
34
- }
35
- return wrapOneRevUnsigned(azimuth);
36
- }
37
-
38
- /**
39
- * Calculate the specific angular momentum (h) vector of an orbit.
40
- * @param {Array} r - Position vector [x, y, z]
41
- * @param {Array} v - Velocity vector [vx, vy, vz]
42
- * @return {Array} Angular momentum vector
43
- */
44
- static getAngularMomentum(r, v) {
45
- return cross(r, v);
46
- }
47
-
48
- /**
49
- * Get mean motion (rad/sec).
50
- * @param {number} a - Semi Major Axis (km)
51
- * @param {number} mu - Standard gravitational parameter (km^3/s^2)
52
- * @return {number} Mean motion in rad/sec
53
- */
54
- static getMeanMotion(a, mu = EARTH_MU_KM) {
55
- return Math.sqrt(mu / Math.pow(a, 3));
56
- }
57
-
58
- /**
59
- * Get the period of an orbit in seconds.
60
- * @param {number} a - Semi Major Axis (km)
61
- * @param {number} mu - Standard gravitational parameter (km^3/s^2)
62
- * @return {number} Period in seconds
63
- */
64
- static getPeriod(a, mu = EARTH_MU_KM) {
65
- if (a <= 0) return null;
66
- return 2 * Math.PI * Math.sqrt(Math.pow(a, 3) / mu);
67
- }
68
-
69
- /**
70
- * Transform an orbit to elliptical with target parameters using Brent's method.
71
- * Accurate port of the C# TransformElliptical method.
72
- * @param {Object} svInitial - Initial state vector {position: [x,y,z], velocity: [vx,vy,vz], epoch: Date}
73
- * @param {number} incRad - Inclination in radians
74
- * @param {number} raanRad - RAAN in radians
75
- * @param {number} targetEcc - Target eccentricity
76
- * @param {number} targetArgOfPeriapsisRad - Target argument of periapsis in radians
77
- * @param {number} targetPerigeeKm - Target perigee in km
78
- * @return {Object} Satellite object with new orbital elements
79
- */
80
- static transformElliptical(svInitial, incRad, raanRad,
81
- targetEcc, targetArgOfPeriapsisRad, targetPerigeeKm) {
82
- // Normalize the position vector
83
- const rNorm = norm(svInitial.position);
84
- const r = svInitial.position.map((x) => x / rNorm);
85
-
86
- // Project the position vector onto the orbital plane
87
- const projectedX = r[0] * Math.cos(raanRad) + r[1] * Math.sin(raanRad);
88
- const projectedY = r[1] * Math.cos(raanRad) - r[0] * Math.sin(raanRad);
89
-
90
- // Calculate the initial true anomaly using the projected coordinates and known argument of periapsis
91
- let nu0 = Math.atan2(projectedY, projectedX) - targetArgOfPeriapsisRad;
92
- nu0 = (nu0 + 2 * Math.PI) % (2 * Math.PI);
93
-
94
- // Define the objective function that we want to minimize
95
- const objectiveFunction = (nu) => {
96
- nu = (nu + 2 * Math.PI) % (2 * Math.PI);
97
-
98
- // Create orbital elements with the trial true anomaly
99
- const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
100
- const elements = {
101
- semiMajorAxis: semiMajorAxis,
102
- eccentricity: targetEcc,
103
- inclination: incRad,
104
- raan: raanRad,
105
- argOfPeriapsis: targetArgOfPeriapsisRad,
106
- trueAnomaly: nu,
107
- epoch: svInitial.epoch,
108
- };
109
-
110
- // Convert elements to state vector
111
- const trialState = this.elementsToStateVector(elements);
112
-
113
- // Normalize the trial position vector
114
- const trialRNorm = norm(trialState.position);
115
- const trialR = trialState.position.map((x) => x / trialRNorm);
116
-
117
- // Scale trial position to match initial position magnitude
118
- const pScaled = trialR.map((x) => x * rNorm);
119
-
120
- // Return the distance between initial and trial positions
121
- return norm(svInitial.position.map((x, i) => x - pScaled[i]));
122
- };
123
-
124
- // Use Brent's method for minimization
125
- const tolerance = 1e-6; // radians
126
- const maxIterations = 1000;
127
-
128
- // Initial bracket around the solution
129
- let a = nu0 - Math.PI;
130
- let b = nu0 + Math.PI;
131
- let c = nu0;
132
-
133
- let fa = objectiveFunction(a);
134
- let fb = objectiveFunction(b);
135
- let fc = objectiveFunction(c);
136
-
137
- let d = 0; let e = 0; let min1; let min2;
138
- let p; let q; let r1; let tol1; let xm;
139
-
140
- for (let iter = 0; iter < maxIterations; iter++) {
141
- if (Math.abs(b - a) < tolerance) {
142
- // Found minimum
143
- const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
144
- return {
145
- semiMajorAxis: semiMajorAxis,
146
- eccentricity: targetEcc,
147
- inclination: incRad,
148
- raan: raanRad,
149
- argOfPeriapsis: targetArgOfPeriapsisRad,
150
- trueAnomaly: c,
151
- epoch: svInitial.epoch,
152
- };
153
- }
154
-
155
- xm = 0.5 * (a + b);
156
- tol1 = tolerance * Math.abs(c) + 1e-10;
157
- min1 = Math.abs(c - a);
158
- min2 = Math.abs(b - c);
159
-
160
- if (min1 < tol1 || min2 < tol1) {
161
- // Found minimum
162
- const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
163
- return {
164
- semiMajorAxis: semiMajorAxis,
165
- eccentricity: targetEcc,
166
- inclination: incRad,
167
- raan: raanRad,
168
- argOfPeriapsis: targetArgOfPeriapsisRad,
169
- trueAnomaly: c,
170
- epoch: svInitial.epoch,
171
- };
172
- }
173
-
174
- // Construct a trial parabolic fit
175
- if (Math.abs(e) >= tol1) {
176
- r1 = (c - a) * (fb - fc);
177
- q = (c - b) * (fa - fc);
178
- p = (c - b) * q - (c - a) * r1;
179
- q = 2.0 * (q - r1);
180
- if (q > 0.0) p = -p;
181
- q = Math.abs(q);
182
- min1 = 3.0 * q * Math.abs(xm);
183
- min2 = Math.abs(e * q);
184
- if (2.0 * p < (min1 < min2 ? min1 : min2)) {
185
- e = d;
186
- d = p / q;
187
- } else {
188
- d = xm;
189
- e = d;
190
- }
191
- } else {
192
- d = xm;
193
- e = d;
194
- }
195
-
196
- a = b;
197
- fa = fb;
198
- if (Math.abs(d) > tol1) {
199
- b += d;
200
- } else {
201
- b += (xm >= 0 ? Math.abs(tol1) : -Math.abs(tol1));
202
- }
203
- fb = objectiveFunction(b);
204
- if ((fb <= fc && c > b) || (fb >= fc && c < b)) {
205
- c = a;
206
- fc = fa;
207
- a = b;
208
- fa = fb;
209
- b = c;
210
- fb = fc;
211
- }
212
- }
213
-
214
- throw new Error("Did not converge within maximum iterations");
215
- }
216
-
217
- /**
218
- * Convert orbital elements to state vector
219
- * @param {Object} elements - Orbital elements
220
- * @return {Object} State vector {position, velocity}
221
- */
222
- static elementsToStateVector(elements) {
223
- const {semiMajorAxis, eccentricity, inclination,
224
- raan, argOfPeriapsis, trueAnomaly} = elements;
225
- const mu = EARTH_MU_KM;
226
-
227
- // Calculate radius
228
- const rMag = semiMajorAxis * (1 - eccentricity * eccentricity)
229
- / (1 + eccentricity * Math.cos(trueAnomaly));
230
-
231
- // Position in orbital plane
232
- const rOrbital = [
233
- rMag * Math.cos(trueAnomaly),
234
- rMag * Math.sin(trueAnomaly),
235
- 0,
236
- ];
237
-
238
- // Velocity in orbital plane
239
- const h = Math.sqrt(mu * semiMajorAxis * (1 - eccentricity * eccentricity));
240
- const vOrbital = [
241
- -mu / h * Math.sin(trueAnomaly),
242
- mu / h * (eccentricity + Math.cos(trueAnomaly)),
243
- 0,
244
- ];
245
-
246
- // Rotation matrices
247
- const cosW = Math.cos(argOfPeriapsis);
248
- const sinW = Math.sin(argOfPeriapsis);
249
- const cosO = Math.cos(raan);
250
- const sinO = Math.sin(raan);
251
- const cosI = Math.cos(inclination);
252
- const sinI = Math.sin(inclination);
253
-
254
- // Transform to inertial frame
255
- const r = [
256
- rOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
257
- - rOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
258
- rOrbital[0] * (cosW * sinO + sinW * cosI * cosO) + rOrbital[1]
259
- * (cosW * cosI * cosO - sinW * sinO),
260
- rOrbital[0] * (sinW * sinI) + rOrbital[1] * (cosW * sinI),
261
- ];
262
-
263
- const v = [
264
- vOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
265
- - vOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
266
- vOrbital[0] * (cosW * sinO + sinW * cosI * cosO)
267
- + vOrbital[1] * (cosW * cosI * cosO - sinW * sinO),
268
- vOrbital[0] * (sinW * sinI) + vOrbital[1] * (cosW * sinI),
269
- ];
270
-
271
- return {position: r, velocity: v};
272
- }
273
-
274
- /**
275
- * Convert state vector (position, velocity) to classical orbital elements
276
- * @param {Array} r - Position vector [x, y, z] in km
277
- * @param {Array} v - Velocity vector [vx, vy, vz] in km/s
278
- * @param {number} mu - Standard gravitational parameter (default: Earth)
279
- * @return {Object} Orbital elements
280
- */
281
- static stateVectorToElements(r, v, mu = EARTH_MU_KM) {
282
- const tol = 1e-9;
283
-
284
- if (mu < 1e-30) {
285
- throw new Error("Mu must be greater than 1e-30.");
286
- }
287
-
288
- // Helper functions
289
- function cross(a, b) {
290
- return [
291
- a[1] * b[2] - a[2] * b[1],
292
- a[2] * b[0] - a[0] * b[2],
293
- a[0] * b[1] - a[1] * b[0],
294
- ];
295
- }
296
-
297
- function dot(a, b) {
298
- return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
299
- }
300
-
301
- function norm(v) {
302
- return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
303
- }
304
-
305
- function clamp(value, min, max) {
306
- return Math.min(Math.max(value, min), max);
307
- }
308
-
309
- // Calculate basic vectors
310
- const h = cross(r, v);
311
- const n = cross([0, 0, 1], h);
312
-
313
- const rLength = norm(r);
314
- const vLength = norm(v);
315
-
316
- if (rLength === 0) throw new Error("Position vector must not be zero.");
317
- if (vLength === 0) throw new Error("Velocity vector must not be zero.");
318
-
319
- // Eccentricity vector calculation (corrected formula)
320
- const vLengthSq = vLength * vLength;
321
- const muOverR = mu / rLength;
322
- const rvDot = dot(r, v);
323
-
324
- const e = [
325
- (1 / mu) * ((vLengthSq - muOverR) * r[0] - rvDot * v[0]),
326
- (1 / mu) * ((vLengthSq - muOverR) * r[1] - rvDot * v[1]),
327
- (1 / mu) * ((vLengthSq - muOverR) * r[2] - rvDot * v[2]),
328
- ];
329
-
330
- const zeta = 0.5 * vLengthSq - muOverR;
331
-
332
- if (zeta === 0) throw new Error("Zeta cannot be zero.");
333
-
334
- const eLength = norm(e);
335
- if (Math.abs(1.0 - eLength) <= tol) {
336
- throw new Error("Parabolic orbit conversion is not supported.");
337
- }
338
-
339
- const a = -mu / (zeta * 2);
340
-
341
- if (Math.abs(a * (1 - eLength)) < 1e-3) {
342
- throw new Error("The state results in a singular conic section "
343
- + "with radius of periapsis less than 1 m.");
344
- }
345
-
346
- const hLength = norm(h);
347
- if (hLength === 0) {
348
- throw new Error(`Cannot convert from Cartesian to Keplerian
349
- - angular momentum is zero.`);
350
- }
351
-
352
- const i = Math.acos(h[2] / hLength);
353
-
354
- if (i >= Math.PI - tol) {
355
- throw new Error("Cannot convert orbit with inclination of 180 degrees.");
356
- }
357
-
358
- let raan = 0;
359
- let w = 0;
360
- let f = 0;
361
-
362
- const nLength = norm(n);
363
-
364
- // CASE 1: Non-circular, Inclined Orbit
365
- if (eLength >= 1e-11 && i >= 1e-11 && i <= Math.PI - 1e-11) {
366
- if (nLength === 0.0) {
367
- throw new Error("Cannot convert from Cartesian to Keplerian "
368
- + "- line-of-nodes vector is a zero vector.");
369
- }
370
-
371
- raan = Math.acos(n[0] / nLength);
372
- if (n[1] < 0) raan = 2 * Math.PI - raan;
373
-
374
- w = Math.acos(dot(n, e) / (nLength * eLength));
375
- if (e[2] < 0) w = 2 * Math.PI - w;
376
-
377
- f = Math.acos(clamp(dot(e, r) / (eLength * rLength), -1, 1));
378
- if (rvDot < 0) f = 2 * Math.PI - f;
379
- } else if (eLength >= 1e-11 && (i < 1e-11 || i > Math.PI - 1e-11)) { // CASE 2: Non-circular, Equatorial Orbit
380
- if (eLength === 0.0) {
381
- throw new Error(`Cannot convert from Cartesian to Keplerian
382
- - eccentricity is zero.`);
383
- }
384
- raan = 0;
385
- w = Math.acos(e[0] / eLength);
386
- if (e[1] < 0) w = 2 * Math.PI - w;
387
-
388
- // For GMT-4446 fix (LOJ: 2014.03.21)
389
- if (i > Math.PI - 1e-11) w *= -1.0;
390
- if (w < 0.0) w += 2 * Math.PI;
391
-
392
- f = Math.acos(clamp(dot(e, r) / (eLength * rLength), -1, 1));
393
- if (rvDot < 0) f = 2 * Math.PI - f;
394
- } else if (eLength < 1e-11 && i >= 1e-11 && i <= Math.PI - 1e-11) { // CASE 3: Circular, Inclined Orbit
395
- if (nLength === 0.0) {
396
- throw new Error("Cannot convert from Cartesian to Keplerian "
397
- + "- line-of-nodes vector is a zero vector.");
398
- }
399
- raan = Math.acos(n[0] / nLength);
400
- if (n[1] < 0) raan = 2 * Math.PI - raan;
401
-
402
- w = 0;
403
-
404
- f = Math.acos(clamp(dot(n, r) / (nLength * rLength), -1, 1));
405
- if (r[2] < 0) f = 2 * Math.PI - f;
406
- } else if (eLength < 1e-11 && (i < 1e-11 || i > Math.PI - 1e-11)) { // CASE 4: Circular, Equatorial Orbit
407
- raan = 0;
408
- w = 0;
409
- f = Math.acos(clamp(r[0] / rLength, -1, 1));
410
- if (r[1] < 0) f = 2 * Math.PI - f;
411
-
412
- // For GMT-4446 fix (LOJ: 2014.03.21)
413
- if (i > Math.PI - 1e-11) f *= -1.0;
414
- if (f < 0.0) f += 2 * Math.PI;
415
- }
416
-
417
- // Calculate additional orbital parameters
418
- const period = 2 * Math.PI * Math.sqrt(Math.pow(Math.abs(a), 3) / mu);
419
- const meanMotion = 2 * Math.PI / period;
420
-
421
- // Convert true anomaly to mean anomaly properly
422
- const eccentricAnomaly = this.trueAnomalyToEccentricAnomaly(f, eLength);
423
- const meanAnomaly = this.eccentricAnomalyToMeanAnomaly(eccentricAnomaly, eLength);
424
-
425
- return {
426
- semiMajorAxis: a, // km
427
- eccentricity: eLength, // dimensionless
428
- inclination: i, // radians
429
- raan: raan, // radians (Right Ascension of Ascending Node)
430
- argOfPeriapsis: w, // radians (Argument of Periapsis)
431
- trueAnomaly: f, // radians
432
- meanAnomaly: meanAnomaly, // radians (simplified)
433
- period: period, // seconds
434
- meanMotion: meanMotion, // rad/s
435
- };
436
- }
437
-
438
- /**
439
- * Convert true anomaly to eccentric anomaly
440
- * @param {number} nu - true anomaly
441
- * @param {number} e - eccentricity
442
- * @return {number} Eccentric Anomaly
443
- */
444
- static trueAnomalyToEccentricAnomaly(nu, e) {
445
- const cosE = (e + Math.cos(nu)) / (1 + e * Math.cos(nu));
446
- const sinE = Math.sqrt(1 - e * e) * Math.sin(nu) / (1 + e * Math.cos(nu));
447
- return Math.atan2(sinE, cosE);
448
- }
449
-
450
- /**
451
- * Convert eccentric anomaly to mean anomaly
452
- * @param {number} E - Eccentric anomaly
453
- * @param {number} e - eccentricity
454
- * @return {number} Mean Anomaly
455
- */
456
- static eccentricAnomalyToMeanAnomaly(E, e) {
457
- return E - e * Math.sin(E);
458
- }
459
-
460
- /**
461
- * Convert mean anomaly to eccentric anomaly (Kepler's equation)
462
- * @param {number} M - Mean anomaly
463
- * @param {number} e - eccentricity
464
- * @param {number} tolerance - numerical tolerance
465
- * @return {number} Eccentric Anomaly
466
- */
467
- static meanAnomalyToEccentricAnomaly(M, e, tolerance = 1e-8) {
468
- let E = M;
469
- let delta = 1;
470
- while (Math.abs(delta) > tolerance) {
471
- delta = (M - E + e * Math.sin(E)) / (1 - e * Math.cos(E));
472
- E += delta;
473
- }
474
- return E;
475
- }
476
-
477
- /**
478
- * Convert eccentric anomaly to true anomaly
479
- * @param {number} E - Eccentric anomaly
480
- * @param {number} e - eccentricity
481
- * @return {number} true anomaly
482
- */
483
- static eccentricAnomalyToTrueAnomaly(E, e) {
484
- const cosNu = (Math.cos(E) - e) / (1 - e * Math.cos(E));
485
- const sinNu = Math.sqrt(1 - e * e) * Math.sin(E) / (1 - e * Math.cos(E));
486
- return Math.atan2(sinNu, cosNu);
487
- }
488
- }
489
-
490
- export {OrbitUtils};
1
+ import {MU} from "./constants.js";
2
+ import {cross, norm} from "mathjs";
3
+ import {wrapHalfRevUnsigned, wrapOneRevUnsigned} from "./utils.js";
4
+
5
+ const EARTH_MU_KM = MU / 1e9;
6
+
7
+ class OrbitUtils {
8
+ constructor() {
9
+ // Prevent instantiation
10
+ }
11
+ /**
12
+ * Get inclination based on azimuth and latitude.
13
+ * @param {number} azimuthRad - Azimuth in radians
14
+ * @param {number} latitudeRad - Latitude in radians
15
+ * @return {number} Inclination in radians
16
+ */
17
+ static getInclination(azimuthRad, latitudeRad) {
18
+ const inclination = Math.acos(Math.cos(latitudeRad) * Math.sin(azimuthRad));
19
+ return wrapHalfRevUnsigned(inclination);
20
+ }
21
+
22
+ /**
23
+ * Get the azimuth based on inclination and latitude.
24
+ * Two results will be possible, based on whether you are Ascending from South to North or Descending from North to South.
25
+ * @param {number} inclinationRad - Inclination in radians
26
+ * @param {number} latitudeRad - Latitude in radians
27
+ * @param {boolean} ascending - Ascending (true) or descending (false)
28
+ * @return {number} Azimuth in radians
29
+ */
30
+ static getAzimuth(inclinationRad, latitudeRad, ascending = true) {
31
+ const azimuth = Math.asin(Math.cos(inclinationRad) / Math.cos(latitudeRad));
32
+ if (!ascending) {
33
+ return wrapOneRevUnsigned(Math.PI - azimuth);
34
+ }
35
+ return wrapOneRevUnsigned(azimuth);
36
+ }
37
+
38
+ /**
39
+ * Calculate the specific angular momentum (h) vector of an orbit.
40
+ * @param {Array} r - Position vector [x, y, z]
41
+ * @param {Array} v - Velocity vector [vx, vy, vz]
42
+ * @return {Array} Angular momentum vector
43
+ */
44
+ static getAngularMomentum(r, v) {
45
+ return cross(r, v);
46
+ }
47
+
48
+ /**
49
+ * Get mean motion (rad/sec).
50
+ * @param {number} a - Semi Major Axis (km)
51
+ * @param {number} mu - Standard gravitational parameter (km^3/s^2)
52
+ * @return {number} Mean motion in rad/sec
53
+ */
54
+ static getMeanMotion(a, mu = EARTH_MU_KM) {
55
+ return Math.sqrt(mu / Math.pow(a, 3));
56
+ }
57
+
58
+ /**
59
+ * Get the period of an orbit in seconds.
60
+ * @param {number} a - Semi Major Axis (km)
61
+ * @param {number} mu - Standard gravitational parameter (km^3/s^2)
62
+ * @return {number} Period in seconds
63
+ */
64
+ static getPeriod(a, mu = EARTH_MU_KM) {
65
+ if (a <= 0) return null;
66
+ return 2 * Math.PI * Math.sqrt(Math.pow(a, 3) / mu);
67
+ }
68
+
69
+ /**
70
+ * Transform an orbit to elliptical with target parameters using Brent's method.
71
+ * Accurate port of the C# TransformElliptical method.
72
+ * @param {Object} svInitial - Initial state vector {position: [x,y,z], velocity: [vx,vy,vz], epoch: Date}
73
+ * @param {number} incRad - Inclination in radians
74
+ * @param {number} raanRad - RAAN in radians
75
+ * @param {number} targetEcc - Target eccentricity
76
+ * @param {number} targetArgOfPeriapsisRad - Target argument of periapsis in radians
77
+ * @param {number} targetPerigeeKm - Target perigee in km
78
+ * @return {Object} Satellite object with new orbital elements
79
+ */
80
+ static transformElliptical(svInitial, incRad, raanRad,
81
+ targetEcc, targetArgOfPeriapsisRad, targetPerigeeKm) {
82
+ // Normalize the position vector
83
+ const rNorm = norm(svInitial.position);
84
+ const r = svInitial.position.map((x) => x / rNorm);
85
+
86
+ // Project the position vector onto the orbital plane
87
+ const projectedX = r[0] * Math.cos(raanRad) + r[1] * Math.sin(raanRad);
88
+ const projectedY = r[1] * Math.cos(raanRad) - r[0] * Math.sin(raanRad);
89
+
90
+ // Calculate the initial true anomaly using the projected coordinates and known argument of periapsis
91
+ let nu0 = Math.atan2(projectedY, projectedX) - targetArgOfPeriapsisRad;
92
+ nu0 = (nu0 + 2 * Math.PI) % (2 * Math.PI);
93
+
94
+ // Define the objective function that we want to minimize
95
+ const objectiveFunction = (nu) => {
96
+ nu = (nu + 2 * Math.PI) % (2 * Math.PI);
97
+
98
+ // Create orbital elements with the trial true anomaly
99
+ const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
100
+ const elements = {
101
+ semiMajorAxis: semiMajorAxis,
102
+ eccentricity: targetEcc,
103
+ inclination: incRad,
104
+ raan: raanRad,
105
+ argOfPeriapsis: targetArgOfPeriapsisRad,
106
+ trueAnomaly: nu,
107
+ epoch: svInitial.epoch,
108
+ };
109
+
110
+ // Convert elements to state vector
111
+ const trialState = this.elementsToStateVector(elements);
112
+
113
+ // Normalize the trial position vector
114
+ const trialRNorm = norm(trialState.position);
115
+ const trialR = trialState.position.map((x) => x / trialRNorm);
116
+
117
+ // Scale trial position to match initial position magnitude
118
+ const pScaled = trialR.map((x) => x * rNorm);
119
+
120
+ // Return the distance between initial and trial positions
121
+ return norm(svInitial.position.map((x, i) => x - pScaled[i]));
122
+ };
123
+
124
+ // Use Brent's method for minimization
125
+ const tolerance = 1e-6; // radians
126
+ const maxIterations = 1000;
127
+
128
+ // Initial bracket around the solution
129
+ let a = nu0 - Math.PI;
130
+ let b = nu0 + Math.PI;
131
+ let c = nu0;
132
+
133
+ let fa = objectiveFunction(a);
134
+ let fb = objectiveFunction(b);
135
+ let fc = objectiveFunction(c);
136
+
137
+ let d = 0; let e = 0; let min1; let min2;
138
+ let p; let q; let r1; let tol1; let xm;
139
+
140
+ for (let iter = 0; iter < maxIterations; iter++) {
141
+ if (Math.abs(b - a) < tolerance) {
142
+ // Found minimum
143
+ const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
144
+ return {
145
+ semiMajorAxis: semiMajorAxis,
146
+ eccentricity: targetEcc,
147
+ inclination: incRad,
148
+ raan: raanRad,
149
+ argOfPeriapsis: targetArgOfPeriapsisRad,
150
+ trueAnomaly: c,
151
+ epoch: svInitial.epoch,
152
+ };
153
+ }
154
+
155
+ xm = 0.5 * (a + b);
156
+ tol1 = tolerance * Math.abs(c) + 1e-10;
157
+ min1 = Math.abs(c - a);
158
+ min2 = Math.abs(b - c);
159
+
160
+ if (min1 < tol1 || min2 < tol1) {
161
+ // Found minimum
162
+ const semiMajorAxis = targetPerigeeKm / (1 - targetEcc);
163
+ return {
164
+ semiMajorAxis: semiMajorAxis,
165
+ eccentricity: targetEcc,
166
+ inclination: incRad,
167
+ raan: raanRad,
168
+ argOfPeriapsis: targetArgOfPeriapsisRad,
169
+ trueAnomaly: c,
170
+ epoch: svInitial.epoch,
171
+ };
172
+ }
173
+
174
+ // Construct a trial parabolic fit
175
+ if (Math.abs(e) >= tol1) {
176
+ r1 = (c - a) * (fb - fc);
177
+ q = (c - b) * (fa - fc);
178
+ p = (c - b) * q - (c - a) * r1;
179
+ q = 2.0 * (q - r1);
180
+ if (q > 0.0) p = -p;
181
+ q = Math.abs(q);
182
+ min1 = 3.0 * q * Math.abs(xm);
183
+ min2 = Math.abs(e * q);
184
+ if (2.0 * p < (min1 < min2 ? min1 : min2)) {
185
+ e = d;
186
+ d = p / q;
187
+ } else {
188
+ d = xm;
189
+ e = d;
190
+ }
191
+ } else {
192
+ d = xm;
193
+ e = d;
194
+ }
195
+
196
+ a = b;
197
+ fa = fb;
198
+ if (Math.abs(d) > tol1) {
199
+ b += d;
200
+ } else {
201
+ b += (xm >= 0 ? Math.abs(tol1) : -Math.abs(tol1));
202
+ }
203
+ fb = objectiveFunction(b);
204
+ if ((fb <= fc && c > b) || (fb >= fc && c < b)) {
205
+ c = a;
206
+ fc = fa;
207
+ a = b;
208
+ fa = fb;
209
+ b = c;
210
+ fb = fc;
211
+ }
212
+ }
213
+
214
+ throw new Error("Did not converge within maximum iterations");
215
+ }
216
+
217
+ /**
218
+ * Convert orbital elements to state vector
219
+ * @param {Object} elements - Orbital elements
220
+ * @return {Object} State vector {position, velocity}
221
+ */
222
+ static elementsToStateVector(elements) {
223
+ const {semiMajorAxis, eccentricity, inclination,
224
+ raan, argOfPeriapsis, trueAnomaly} = elements;
225
+ const mu = EARTH_MU_KM;
226
+
227
+ // Calculate radius
228
+ const rMag = semiMajorAxis * (1 - eccentricity * eccentricity)
229
+ / (1 + eccentricity * Math.cos(trueAnomaly));
230
+
231
+ // Position in orbital plane
232
+ const rOrbital = [
233
+ rMag * Math.cos(trueAnomaly),
234
+ rMag * Math.sin(trueAnomaly),
235
+ 0,
236
+ ];
237
+
238
+ // Velocity in orbital plane
239
+ const h = Math.sqrt(mu * semiMajorAxis * (1 - eccentricity * eccentricity));
240
+ const vOrbital = [
241
+ -mu / h * Math.sin(trueAnomaly),
242
+ mu / h * (eccentricity + Math.cos(trueAnomaly)),
243
+ 0,
244
+ ];
245
+
246
+ // Rotation matrices
247
+ const cosW = Math.cos(argOfPeriapsis);
248
+ const sinW = Math.sin(argOfPeriapsis);
249
+ const cosO = Math.cos(raan);
250
+ const sinO = Math.sin(raan);
251
+ const cosI = Math.cos(inclination);
252
+ const sinI = Math.sin(inclination);
253
+
254
+ // Transform to inertial frame
255
+ const r = [
256
+ rOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
257
+ - rOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
258
+ rOrbital[0] * (cosW * sinO + sinW * cosI * cosO) + rOrbital[1]
259
+ * (cosW * cosI * cosO - sinW * sinO),
260
+ rOrbital[0] * (sinW * sinI) + rOrbital[1] * (cosW * sinI),
261
+ ];
262
+
263
+ const v = [
264
+ vOrbital[0] * (cosW * cosO - sinW * cosI * sinO)
265
+ - vOrbital[1] * (sinW * cosO + cosW * cosI * sinO),
266
+ vOrbital[0] * (cosW * sinO + sinW * cosI * cosO)
267
+ + vOrbital[1] * (cosW * cosI * cosO - sinW * sinO),
268
+ vOrbital[0] * (sinW * sinI) + vOrbital[1] * (cosW * sinI),
269
+ ];
270
+
271
+ return {position: r, velocity: v};
272
+ }
273
+
274
+ /**
275
+ * Convert state vector (position, velocity) to classical orbital elements
276
+ * @param {Array} r - Position vector [x, y, z] in km
277
+ * @param {Array} v - Velocity vector [vx, vy, vz] in km/s
278
+ * @param {number} mu - Standard gravitational parameter (default: Earth)
279
+ * @return {Object} Orbital elements
280
+ */
281
+ static stateVectorToElements(r, v, mu = EARTH_MU_KM) {
282
+ const tol = 1e-9;
283
+
284
+ if (mu < 1e-30) {
285
+ throw new Error("Mu must be greater than 1e-30.");
286
+ }
287
+
288
+ // Helper functions
289
+ function cross(a, b) {
290
+ return [
291
+ a[1] * b[2] - a[2] * b[1],
292
+ a[2] * b[0] - a[0] * b[2],
293
+ a[0] * b[1] - a[1] * b[0],
294
+ ];
295
+ }
296
+
297
+ function dot(a, b) {
298
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
299
+ }
300
+
301
+ function norm(v) {
302
+ return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
303
+ }
304
+
305
+ function clamp(value, min, max) {
306
+ return Math.min(Math.max(value, min), max);
307
+ }
308
+
309
+ // Calculate basic vectors
310
+ const h = cross(r, v);
311
+ const n = cross([0, 0, 1], h);
312
+
313
+ const rLength = norm(r);
314
+ const vLength = norm(v);
315
+
316
+ if (rLength === 0) throw new Error("Position vector must not be zero.");
317
+ if (vLength === 0) throw new Error("Velocity vector must not be zero.");
318
+
319
+ // Eccentricity vector calculation (corrected formula)
320
+ const vLengthSq = vLength * vLength;
321
+ const muOverR = mu / rLength;
322
+ const rvDot = dot(r, v);
323
+
324
+ const e = [
325
+ (1 / mu) * ((vLengthSq - muOverR) * r[0] - rvDot * v[0]),
326
+ (1 / mu) * ((vLengthSq - muOverR) * r[1] - rvDot * v[1]),
327
+ (1 / mu) * ((vLengthSq - muOverR) * r[2] - rvDot * v[2]),
328
+ ];
329
+
330
+ const zeta = 0.5 * vLengthSq - muOverR;
331
+
332
+ if (zeta === 0) throw new Error("Zeta cannot be zero.");
333
+
334
+ const eLength = norm(e);
335
+ if (Math.abs(1.0 - eLength) <= tol) {
336
+ throw new Error("Parabolic orbit conversion is not supported.");
337
+ }
338
+
339
+ const a = -mu / (zeta * 2);
340
+
341
+ if (Math.abs(a * (1 - eLength)) < 1e-3) {
342
+ throw new Error("The state results in a singular conic section "
343
+ + "with radius of periapsis less than 1 m.");
344
+ }
345
+
346
+ const hLength = norm(h);
347
+ if (hLength === 0) {
348
+ throw new Error(`Cannot convert from Cartesian to Keplerian
349
+ - angular momentum is zero.`);
350
+ }
351
+
352
+ const i = Math.acos(h[2] / hLength);
353
+
354
+ if (i >= Math.PI - tol) {
355
+ throw new Error("Cannot convert orbit with inclination of 180 degrees.");
356
+ }
357
+
358
+ let raan = 0;
359
+ let w = 0;
360
+ let f = 0;
361
+
362
+ const nLength = norm(n);
363
+
364
+ // CASE 1: Non-circular, Inclined Orbit
365
+ if (eLength >= 1e-11 && i >= 1e-11 && i <= Math.PI - 1e-11) {
366
+ if (nLength === 0.0) {
367
+ throw new Error("Cannot convert from Cartesian to Keplerian "
368
+ + "- line-of-nodes vector is a zero vector.");
369
+ }
370
+
371
+ raan = Math.acos(n[0] / nLength);
372
+ if (n[1] < 0) raan = 2 * Math.PI - raan;
373
+
374
+ w = Math.acos(dot(n, e) / (nLength * eLength));
375
+ if (e[2] < 0) w = 2 * Math.PI - w;
376
+
377
+ f = Math.acos(clamp(dot(e, r) / (eLength * rLength), -1, 1));
378
+ if (rvDot < 0) f = 2 * Math.PI - f;
379
+ } else if (eLength >= 1e-11 && (i < 1e-11 || i > Math.PI - 1e-11)) { // CASE 2: Non-circular, Equatorial Orbit
380
+ if (eLength === 0.0) {
381
+ throw new Error(`Cannot convert from Cartesian to Keplerian
382
+ - eccentricity is zero.`);
383
+ }
384
+ raan = 0;
385
+ w = Math.acos(e[0] / eLength);
386
+ if (e[1] < 0) w = 2 * Math.PI - w;
387
+
388
+ // For GMT-4446 fix (LOJ: 2014.03.21)
389
+ if (i > Math.PI - 1e-11) w *= -1.0;
390
+ if (w < 0.0) w += 2 * Math.PI;
391
+
392
+ f = Math.acos(clamp(dot(e, r) / (eLength * rLength), -1, 1));
393
+ if (rvDot < 0) f = 2 * Math.PI - f;
394
+ } else if (eLength < 1e-11 && i >= 1e-11 && i <= Math.PI - 1e-11) { // CASE 3: Circular, Inclined Orbit
395
+ if (nLength === 0.0) {
396
+ throw new Error("Cannot convert from Cartesian to Keplerian "
397
+ + "- line-of-nodes vector is a zero vector.");
398
+ }
399
+ raan = Math.acos(n[0] / nLength);
400
+ if (n[1] < 0) raan = 2 * Math.PI - raan;
401
+
402
+ w = 0;
403
+
404
+ f = Math.acos(clamp(dot(n, r) / (nLength * rLength), -1, 1));
405
+ if (r[2] < 0) f = 2 * Math.PI - f;
406
+ } else if (eLength < 1e-11 && (i < 1e-11 || i > Math.PI - 1e-11)) { // CASE 4: Circular, Equatorial Orbit
407
+ raan = 0;
408
+ w = 0;
409
+ f = Math.acos(clamp(r[0] / rLength, -1, 1));
410
+ if (r[1] < 0) f = 2 * Math.PI - f;
411
+
412
+ // For GMT-4446 fix (LOJ: 2014.03.21)
413
+ if (i > Math.PI - 1e-11) f *= -1.0;
414
+ if (f < 0.0) f += 2 * Math.PI;
415
+ }
416
+
417
+ // Calculate additional orbital parameters
418
+ const period = 2 * Math.PI * Math.sqrt(Math.pow(Math.abs(a), 3) / mu);
419
+ const meanMotion = 2 * Math.PI / period;
420
+
421
+ // Convert true anomaly to mean anomaly properly
422
+ const eccentricAnomaly = this.trueAnomalyToEccentricAnomaly(f, eLength);
423
+ const meanAnomaly = this.eccentricAnomalyToMeanAnomaly(eccentricAnomaly, eLength);
424
+
425
+ return {
426
+ semiMajorAxis: a, // km
427
+ eccentricity: eLength, // dimensionless
428
+ inclination: i, // radians
429
+ raan: raan, // radians (Right Ascension of Ascending Node)
430
+ argOfPeriapsis: w, // radians (Argument of Periapsis)
431
+ trueAnomaly: f, // radians
432
+ meanAnomaly: meanAnomaly, // radians (simplified)
433
+ period: period, // seconds
434
+ meanMotion: meanMotion, // rad/s
435
+ };
436
+ }
437
+
438
+ /**
439
+ * Convert true anomaly to eccentric anomaly
440
+ * @param {number} nu - true anomaly
441
+ * @param {number} e - eccentricity
442
+ * @return {number} Eccentric Anomaly
443
+ */
444
+ static trueAnomalyToEccentricAnomaly(nu, e) {
445
+ const cosE = (e + Math.cos(nu)) / (1 + e * Math.cos(nu));
446
+ const sinE = Math.sqrt(1 - e * e) * Math.sin(nu) / (1 + e * Math.cos(nu));
447
+ return Math.atan2(sinE, cosE);
448
+ }
449
+
450
+ /**
451
+ * Convert eccentric anomaly to mean anomaly
452
+ * @param {number} E - Eccentric anomaly
453
+ * @param {number} e - eccentricity
454
+ * @return {number} Mean Anomaly
455
+ */
456
+ static eccentricAnomalyToMeanAnomaly(E, e) {
457
+ return E - e * Math.sin(E);
458
+ }
459
+
460
+ /**
461
+ * Convert mean anomaly to eccentric anomaly (Kepler's equation)
462
+ * @param {number} M - Mean anomaly
463
+ * @param {number} e - eccentricity
464
+ * @param {number} tolerance - numerical tolerance
465
+ * @return {number} Eccentric Anomaly
466
+ */
467
+ static meanAnomalyToEccentricAnomaly(M, e, tolerance = 1e-8) {
468
+ let E = M;
469
+ let delta = 1;
470
+ while (Math.abs(delta) > tolerance) {
471
+ delta = (M - E + e * Math.sin(E)) / (1 - e * Math.cos(E));
472
+ E += delta;
473
+ }
474
+ return E;
475
+ }
476
+
477
+ /**
478
+ * Convert eccentric anomaly to true anomaly
479
+ * @param {number} E - Eccentric anomaly
480
+ * @param {number} e - eccentricity
481
+ * @return {number} true anomaly
482
+ */
483
+ static eccentricAnomalyToTrueAnomaly(E, e) {
484
+ const cosNu = (Math.cos(E) - e) / (1 - e * Math.cos(E));
485
+ const sinNu = Math.sqrt(1 - e * e) * Math.sin(E) / (1 - e * Math.cos(E));
486
+ return Math.atan2(sinNu, cosNu);
487
+ }
488
+ }
489
+
490
+ export {OrbitUtils};