@saber-usa/node-common 1.7.5 → 1.7.7-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saber-usa/node-common",
3
- "version": "1.7.5",
3
+ "version": "1.7.7-alpha.1",
4
4
  "description": "Common node functions for Saber",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -9,7 +9,8 @@
9
9
  "lint:fix": "eslint . --ext js --fix",
10
10
  "test": "jest --no-coverage --silent",
11
11
  "test:unit": "jest --coverage --runInBand --no-watch",
12
- "sonar": "node --experimental-vm-modules sonar-project.js"
12
+ "sonar": "node --experimental-vm-modules sonar-project.js",
13
+ "prepub:alpha": "npm version prerelease --preid=alpha && npm publish --tag alpha"
13
14
  },
14
15
  "files": [
15
16
  "src/**/*"
@@ -3,6 +3,7 @@ import {Matrix3D, Vector3D} from "pious-squid";
3
3
  import {LLA} from "./LLA.js";
4
4
  import {TimeConverter, RaDec} from "./TimeConverter.js";
5
5
  import {norm, cross} from "mathjs";
6
+ import {DEG2RAD} from "./constants.js";
6
7
 
7
8
 
8
9
  // Enums (assuming these exist elsewhere)
@@ -957,12 +958,12 @@ class FrameConverter {
957
958
  }
958
959
 
959
960
  static perifocalToInertial(keplerianElements) {
960
- const cosi = Math.cos(keplerianElements.inclination);
961
- const sini = Math.sin(keplerianElements.inclination);
962
- const cosRaan = Math.cos(keplerianElements.raan);
963
- const sinRaan = Math.sin(keplerianElements.raan);
964
- const cosw = Math.cos(keplerianElements.argOfPeriapsis);
965
- const sinw = Math.sin(keplerianElements.argOfPeriapsis);
961
+ const cosi = Math.cos(keplerianElements.i * DEG2RAD);
962
+ const sini = Math.sin(keplerianElements.i * DEG2RAD);
963
+ const cosRaan = Math.cos(keplerianElements.raan * DEG2RAD);
964
+ const sinRaan = Math.sin(keplerianElements.raan * DEG2RAD);
965
+ const cosw = Math.cos(keplerianElements.w * DEG2RAD);
966
+ const sinw = Math.sin(keplerianElements.w * DEG2RAD);
966
967
 
967
968
  // The matrix elements
968
969
  const r11 = cosRaan * cosw - sinRaan * sinw * cosi;
@@ -5,12 +5,14 @@ import {WGS84_EARTH_EQUATORIAL_RADIUS_KM,
5
5
  GRAV_CONST,
6
6
  EARTH_MASS,
7
7
  MU,
8
- EARTH_RADIUS_KM} from "./constants.js";
8
+ EARTH_RADIUS_KM,
9
+ DEG2RAD} from "./constants.js";
9
10
  import {FrameConverter} from "./FrameConverter.js";
10
11
  import {OrbitUtils} from "./OrbitUtils.js";
11
12
  import {wrapOneRevUnsigned} from "./utils.js";
12
13
  import {BallisticPropagator} from "./ballisticPropagator.js";
13
14
  import {norm, cross, dot} from "mathjs";
15
+ import {cartesianToKeplerian, keplerianToCartesian} from "./astro.js";
14
16
 
15
17
  // Reference frames enum
16
18
  const ReferenceFrame = {
@@ -20,9 +22,9 @@ const ReferenceFrame = {
20
22
  GCRF: "GCRF",
21
23
  };
22
24
 
23
- class LaunchNominalClass {
24
-
25
+ const mu = GRAV_CONST * EARTH_MASS / 1e9; // km^3/s^2
25
26
 
27
+ class LaunchNominalClass {
26
28
  /** Get position at a given time in specified reference frame
27
29
  *
28
30
  * @param {Date} utc - UTC time
@@ -56,7 +58,6 @@ class LaunchNominalClass {
56
58
  let bearingRad = degreesToRadians(bearingDeg);
57
59
  bearingRad = wrapOneRevUnsigned(bearingRad);
58
60
 
59
- // const latRad = degreesToRadians(this.groundSiteLat);
60
61
  const latRad = llaVals.Latitude.Radians;
61
62
  const targetInclination = OrbitUtils.getInclination(bearingRad, latRad);
62
63
 
@@ -77,23 +78,7 @@ class LaunchNominalClass {
77
78
  ReferenceFrame.J2000,
78
79
  llaVals,
79
80
  );
80
- // Trying something here
81
-
82
- // const transformMatrix = FrameConverter.j2000ToTEME(orbitInsertionTime);
83
- // const initialTemeState = {
84
- // position: this.transformVector(
85
- // new Vector3D(initialStateVector.position[0],
86
- // initialStateVector.position[1], initialStateVector.position[2]),
87
- // transformMatrix
88
- // ),
89
- // velocity: this.transformVector(
90
- // new Vector3D(initialStateVector.velocity[0],
91
- // initialStateVector.velocity[1], initialStateVector.velocity[2]),
92
- // transformMatrix
93
- // ),
94
- // };
95
-
96
- // End of something
81
+
97
82
  const initialJ2000State = new J2000(
98
83
  EpochUTC.fromDateString(orbitInsertionTime.toISOString()),
99
84
  new Vector3D(initialStateVector.position[0],
@@ -106,11 +91,11 @@ class LaunchNominalClass {
106
91
 
107
92
  let sat = {
108
93
  stateVector: initialStateVector,
109
- elements: OrbitUtils.stateVectorToElements(
94
+ elements: cartesianToKeplerian(
110
95
  [initialTemeState.position.x, initialTemeState.position.y,
111
96
  initialTemeState.position.z],
112
97
  [initialTemeState.velocity.x, initialTemeState.velocity.y,
113
- initialTemeState.velocity.z]),
98
+ initialTemeState.velocity.z], mu),
114
99
  epoch: orbitInsertionTime,
115
100
  };
116
101
 
@@ -152,12 +137,12 @@ class LaunchNominalClass {
152
137
 
153
138
  const temeState = propagatedJ2000State.toTEME();
154
139
 
155
- // Convert to orbital elements
156
- const elements = OrbitUtils.stateVectorToElements(
140
+ // Convert to orbital elements - angles are in degrees
141
+ const elements = cartesianToKeplerian(
157
142
  [temeState.position.x, temeState.position.y,
158
143
  temeState.position.z],
159
144
  [temeState.velocity.x, temeState.velocity.y,
160
- temeState.velocity.z]);
145
+ temeState.velocity.z], mu);
161
146
 
162
147
  sat = {
163
148
  stateVector: propagated,
@@ -167,7 +152,7 @@ class LaunchNominalClass {
167
152
 
168
153
  // Calculate current inclination
169
154
  oldInclinationDelta = inclinationDelta;
170
- inclinationDelta = Math.abs(radiansToDegrees(sat.elements.inclination)
155
+ inclinationDelta = Math.abs((sat.elements.i)
171
156
  - radiansToDegrees(targetInclination));
172
157
 
173
158
  if (oldInclinationDelta < inclinationDelta) {
@@ -240,22 +225,22 @@ class LaunchNominalClass {
240
225
  // Transform to elliptical orbit
241
226
  const ellipticalElements = OrbitUtils.transformElliptical(
242
227
  svInitial,
243
- initialNominal.states[0].elements.inc,
244
- initialNominal.states[0].elements.raan,
228
+ initialNominal.states[0].elements.inc * DEG2RAD,
229
+ initialNominal.states[0].elements.raan * DEG2RAD,
245
230
  targetEcc,
246
- degreesToRadians(targetArgOfPeriapsisDeg),
231
+ targetArgOfPeriapsisDeg * DEG2RAD,
247
232
  targetPerigeeKm + WGS84_EARTH_EQUATORIAL_RADIUS_KM,
248
233
  );
249
234
 
250
235
  // Convert elliptical elements to cartesian state
251
- const ellipticalStateVector = OrbitUtils.elementsToStateVector(ellipticalElements);
236
+ const ellipticalStateVector = keplerianToCartesian(ellipticalElements, mu);
252
237
 
253
- const ellipticalPos = ellipticalStateVector.position;
254
- const ellipticalVel = ellipticalStateVector.velocity;
238
+ const ellipticalPos = ellipticalStateVector.r;
239
+ const ellipticalVel = ellipticalStateVector.v;
255
240
 
256
241
  const ellipticalState = {
257
- position: new NodeVector3D(ellipticalPos[0], ellipticalPos[1], ellipticalPos[2]),
258
- velocity: new NodeVector3D(ellipticalVel[0], ellipticalVel[1], ellipticalVel[2]),
242
+ position: new NodeVector3D(ellipticalPos.x, ellipticalPos.y, ellipticalPos.z),
243
+ velocity: new NodeVector3D(ellipticalVel.x, ellipticalVel.y, ellipticalVel.z),
259
244
  epochUtc: orbitInsertionTime,
260
245
  referenceFrame: "J2000",
261
246
  };
@@ -268,7 +253,7 @@ class LaunchNominalClass {
268
253
  const ellipNomVel = ellipticalNominal.velocity;
269
254
 
270
255
  // Convert to orbital elements
271
- const elements = OrbitUtils.stateVectorToElements(
256
+ const elements = cartesianToKeplerian(
272
257
  [ellipNomPos.x, ellipNomPos.y, ellipNomPos.z],
273
258
  [ellipNomVel.x, ellipNomVel.y, ellipNomVel.z]);
274
259
 
@@ -408,7 +393,7 @@ class LaunchNominalClass {
408
393
  }
409
394
 
410
395
  static hohmannTransferWithIncZeroing(state0, targetSMA,
411
- burnAtNodes = 1, mu = MU / 1e9) {
396
+ burnAtNodes = 1, mu = MU) {
412
397
  let t0 = state0.epochUtc;
413
398
  let i = null;
414
399
 
@@ -473,8 +458,8 @@ class LaunchNominalClass {
473
458
 
474
459
  const rElementConv = [state0.position.x, state0.position.y, state0.position.z];
475
460
  const vElementConv = [state0.velocity.x, state0.velocity.y, state0.velocity.z];
476
- const elements0 = OrbitUtils.stateVectorToElements(rElementConv, vElementConv);
477
- const incDiff = elements0.inclination;
461
+ const elements0 = cartesianToKeplerian(rElementConv, vElementConv, mu);
462
+ const incDiff = elements0.i * DEG2RAD;
478
463
 
479
464
  const h = cross([state0.position.x, state0.position.y, state0.position.z],
480
465
  [state0.velocity.x, state0.velocity.y, state0.velocity.z]);
@@ -540,41 +525,34 @@ class LaunchNominalClass {
540
525
  }
541
526
 
542
527
  static #getNextNodeCrossingGeoOrbit(sv) {
543
- const geoElements = {
544
- semiMajorAxis: 35786.0 + EARTH_RADIUS_KM,
545
- eccentricity: 0,
546
- inclination: 0,
547
- raan: 0,
548
- argOfPeriapsis: 0,
549
- trueAnomaly: 0,
550
- };
528
+ const geoElements = [35786 + EARTH_RADIUS_KM, 0, 0, 0, 0, 0];
551
529
  const geoOrbitState = {
552
- state: OrbitUtils.elementsToStateVector(geoElements),
530
+ state: keplerianToCartesian(geoElements, mu),
553
531
  epochUtc: sv.epochUtc,
554
532
  };
555
533
  const nextNode = this.#findMutualNodeTimes(sv, geoOrbitState);
556
534
  return nextNode.nextNodeTime;
557
535
  }
558
536
 
559
- static #findMutualNodeTimes(sv1, sv2, muEarth = MU / 1e9) {
537
+ static #findMutualNodeTimes(sv1, sv2, muEarth = MU) {
560
538
  // Implementation for finding mutual node times between two state vectors
561
539
 
562
540
  if (!(sv1.epochUtc === sv2.epochUtc)) {
563
541
  return "lol what on earth";
564
542
  }
565
543
 
566
- const orbit1 = OrbitUtils.stateVectorToElements(
544
+ const orbit1 = cartesianToKeplerian(
567
545
  [sv1.position.x, sv1.position.y, sv1.position.z],
568
- [sv1.velocity.x, sv1.velocity.y, sv1.velocity.z]);
569
- const orbit2 = OrbitUtils.stateVectorToElements(
570
- sv2.state.position,
571
- sv2.state.velocity);
546
+ [sv1.velocity.x, sv1.velocity.y, sv1.velocity.z], muEarth);
572
547
 
573
- const state1 = OrbitUtils.elementsToStateVector(orbit1);
574
- const state2 = OrbitUtils.elementsToStateVector(orbit2);
548
+ const orbit2 = cartesianToKeplerian(
549
+ [sv2.state.r.x, sv2.state.r.y, sv2.state.r.z],
550
+ [sv2.state.v.x, sv2.state.v.y, sv2.state.v.z], muEarth);
575
551
 
576
- const h1 = cross(state1.position, state1.velocity);
577
- const h2 = cross(state2.position, state2.velocity);
552
+ const state1 = keplerianToCartesian(Object.values(orbit1), muEarth);
553
+ const state2 = keplerianToCartesian(Object.values(orbit2), muEarth);
554
+ const h1 = cross(Object.values(state1.r), Object.values(state1.v));
555
+ const h2 = cross(Object.values(state2.r), Object.values(state2.v));
578
556
  const h1Norm = norm(h1);
579
557
  const h2Norm = norm(h2);
580
558
 
@@ -587,7 +565,8 @@ class LaunchNominalClass {
587
565
  return Math.acos(dotProduct / normsProduct);
588
566
  }
589
567
 
590
- const ascendingFirst = getAngleBetween2Vectors(state1.position, h2unit) > Math.PI / 2;
568
+ const ascendingFirst
569
+ = getAngleBetween2Vectors(Object.values(state1.r), h2unit) > Math.PI / 2;
591
570
 
592
571
  const nodalLine = cross(h1unit, h2unit);
593
572
  const nodalLineUnit = nodalLine.map((component) => component / norm(nodalLine));
@@ -602,10 +581,10 @@ class LaunchNominalClass {
602
581
 
603
582
  const mutualNodeLinePerifocal = rTilde1inverse.multiplyVector3D(nodalLineUnitVector3D);
604
583
  const slopeMutualNodalLinePerifocal = mutualNodeLinePerifocal.y / mutualNodeLinePerifocal.x;
605
- const mutualNode = this.#findIntersectionEllipseLine( // inputs here are causing an error increase
584
+ const mutualNode = this.#findIntersectionEllipseLine(
606
585
  slopeMutualNodalLinePerifocal,
607
- orbit1.semiMajorAxis,
608
- orbit1.eccentricity);
586
+ orbit1.a,
587
+ orbit1.e);
609
588
 
610
589
  const q1 = mutualNode.vec1;
611
590
  const q2 = mutualNode.vec2;
@@ -616,13 +595,13 @@ class LaunchNominalClass {
616
595
  return ((n % m) + m) % m;
617
596
  }
618
597
 
619
- const n0 = mod(orbit1.trueAnomaly, 2 * Math.PI);
598
+ const n0 = mod(orbit1.f * DEG2RAD, 2 * Math.PI);
620
599
  const n1 = mod(Math.atan2(dot(q1unit, [0, 1, 0]), dot(q1unit, [1, 0, 0])), 2 * Math.PI);
621
600
  const n2 = mod(Math.atan2(dot(q2unit, [0, 1, 0]), dot(q2unit, [1, 0, 0])), 2 * Math.PI);
622
601
 
623
- const e0 = OrbitUtils.trueAnomalyToEccentricAnomaly(n0, orbit1.eccentricity);
624
- let e1 = OrbitUtils.trueAnomalyToEccentricAnomaly(n1, orbit1.eccentricity); // only matching to 2 sig figs w astrolib
625
- let e2 = OrbitUtils.trueAnomalyToEccentricAnomaly(n2, orbit1.eccentricity); // only matching to 3 sig figs w astrolib
602
+ const e0 = OrbitUtils.trueAnomalyToEccentricAnomaly(n0, orbit1.e);
603
+ let e1 = OrbitUtils.trueAnomalyToEccentricAnomaly(n1, orbit1.e); // only matching to 2 sig figs w astrolib
604
+ let e2 = OrbitUtils.trueAnomalyToEccentricAnomaly(n2, orbit1.e); // only matching to 3 sig figs w astrolib
626
605
 
627
606
  if (e1 < 0) {
628
607
  e1 += 2 * Math.PI;
@@ -635,13 +614,13 @@ class LaunchNominalClass {
635
614
  const k1 = n1 < n0 ? 1 : 0;
636
615
  const k2 = n2 < n0 ? 1 : 0;
637
616
 
638
- const dt1 = Math.sqrt(Math.pow(orbit1.semiMajorAxis, 3) / muEarth)
639
- * (2 * k1 * Math.PI + (e1 - orbit1.eccentricity * Math.sin(e1))
640
- - (e0 - orbit1.eccentricity * Math.sin(e0)));
617
+ const dt1 = Math.sqrt(Math.pow(orbit1.a, 3) / muEarth)
618
+ * (2 * k1 * Math.PI + (e1 - orbit1.e * Math.sin(e1))
619
+ - (e0 - orbit1.e * Math.sin(e0)));
641
620
  const dt2 = Math.sqrt(
642
- Math.pow(orbit1.semiMajorAxis, 3) / muEarth)
643
- * (2 * k2 * Math.PI + (e2 - orbit1.eccentricity * Math.sin(e2))
644
- - (e0 - orbit1.eccentricity * Math.sin(e0)));
621
+ Math.pow(orbit1.a, 3) / muEarth)
622
+ * (2 * k2 * Math.PI + (e2 - orbit1.e * Math.sin(e2))
623
+ - (e0 - orbit1.e * Math.sin(e0)));
645
624
  let first = null;
646
625
  let second = null;
647
626
 
@@ -721,7 +700,7 @@ class LaunchNominalOutput {
721
700
  const j2000ToTeme = FrameConverter.getTransform("J2000", "TEME", epoch);
722
701
  const posTeme = FrameConverter.transformVector(pos, j2000ToTeme);
723
702
  const velTeme = FrameConverter.transformVector(vel, j2000ToTeme);
724
- elements = OrbitUtils.stateVectorToElements(
703
+ elements = cartesianToKeplerian(
725
704
  [posTeme.x, posTeme.y, posTeme.z],
726
705
  [velTeme.x, velTeme.y, velTeme.z]);
727
706
  }
@@ -733,12 +712,12 @@ class LaunchNominalOutput {
733
712
  v: [vel.x, vel.y, vel.z],
734
713
  },
735
714
  elements: {
736
- sma: elements.semiMajorAxis,
737
- ecc: elements.eccentricity,
738
- inc: elements.inclination,
715
+ sma: elements.a,
716
+ ecc: elements.e,
717
+ inc: elements.i,
739
718
  raan: elements.raan,
740
- argp: elements.argOfPeriapsis,
741
- ta: elements.trueAnomaly,
719
+ argp: elements.w,
720
+ ta: elements.f,
742
721
  },
743
722
  // Placeholder for tle
744
723
  });
package/src/OrbitUtils.js CHANGED
@@ -1,13 +1,10 @@
1
- import {MU} from "./constants.js";
1
+ import {MU, RAD2DEG} from "./constants.js";
2
2
  import {cross, norm} from "mathjs";
3
3
  import {wrapHalfRevUnsigned, wrapOneRevUnsigned} from "./utils.js";
4
4
 
5
- const EARTH_MU_KM = MU / 1e9;
5
+ const EARTH_MU_KM = MU; // km³/s²
6
6
 
7
7
  class OrbitUtils {
8
- constructor() {
9
- // Prevent instantiation
10
- }
11
8
  /**
12
9
  * Get inclination based on azimuth and latitude.
13
10
  * @param {number} azimuthRad - Azimuth in radians
@@ -141,15 +138,8 @@ class OrbitUtils {
141
138
  if (Math.abs(b - a) < tolerance) {
142
139
  // Found minimum
143
140
  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
- };
141
+ return [semiMajorAxis, targetEcc, incRad * RAD2DEG,
142
+ raanRad * RAD2DEG, targetArgOfPeriapsisRad * RAD2DEG, c * RAD2DEG];
153
143
  }
154
144
 
155
145
  xm = 0.5 * (a + b);
@@ -160,15 +150,8 @@ class OrbitUtils {
160
150
  if (min1 < tol1 || min2 < tol1) {
161
151
  // Found minimum
162
152
  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
- };
153
+ return [semiMajorAxis, targetEcc, incRad * RAD2DEG,
154
+ raanRad * RAD2DEG, targetArgOfPeriapsisRad * RAD2DEG, c * RAD2DEG];
172
155
  }
173
156
 
174
157
  // Construct a trial parabolic fit
@@ -271,170 +254,6 @@ class OrbitUtils {
271
254
  return {position: r, velocity: v};
272
255
  }
273
256
 
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
257
  /**
439
258
  * Convert true anomaly to eccentric anomaly
440
259
  * @param {number} nu - true anomaly
@@ -6,8 +6,8 @@ import {RungeKutta4Propagator,
6
6
  ClassicalElements} from "pious-squid";
7
7
  import {sgp4} from "satellite.js";
8
8
  import {NodeVector3D} from "./NodeVector3D.js";
9
- import {checkTle} from "./astro.js";
10
- import {OrbitUtils} from "./OrbitUtils.js";
9
+ import {cartesianToKeplerian, checkTle} from "./astro.js";
10
+ import {DEG2RAD} from "./constants.js";
11
11
 
12
12
  const ReferenceFrame = {
13
13
  ITRF: "ITRF",
@@ -62,17 +62,17 @@ class PropagateUtils {
62
62
  }
63
63
 
64
64
  static keplerianPropagator(initialState, time) {
65
- const elementsObj = OrbitUtils.stateVectorToElements(
65
+ const elementsObjReal = cartesianToKeplerian(
66
66
  initialState.position, initialState.velocity);
67
67
 
68
68
  const elements = new ClassicalElements(
69
69
  new EpochUTC(initialState.epoch),
70
- elementsObj.semiMajorAxis,
71
- elementsObj.eccentricity,
72
- elementsObj.inclination,
73
- elementsObj.raan,
74
- elementsObj.argOfPeriapsis,
75
- elementsObj.trueAnomaly,
70
+ elementsObjReal.a,
71
+ elementsObjReal.e,
72
+ elementsObjReal.i * DEG2RAD,
73
+ elementsObjReal.raan * DEG2RAD,
74
+ elementsObjReal.w * DEG2RAD,
75
+ elementsObjReal.f * DEG2RAD,
76
76
  );
77
77
 
78
78
  // Now pass to KeplerPropagator:
package/src/astro.js CHANGED
@@ -42,7 +42,11 @@ import {DEG2RAD,
42
42
  WGS72_EARTH_EQUATORIAL_RADIUS_KM,
43
43
  WGS84_EARTH_EQUATORIAL_RADIUS_KM,
44
44
  MILLIS_PER_DAY,
45
- GEO_ALTITUDE_KM} from "./constants.js";
45
+ GEO_ALTITUDE_KM,
46
+ MU,
47
+ MU_GRS80,
48
+ MU_SI,
49
+ ERROR_CODES} from "./constants.js";
46
50
 
47
51
  // Solar Terminator
48
52
  // Returns sun latitude and longitude as -180/180
@@ -74,6 +78,98 @@ const checkTle = (line1, line2) => {
74
78
  }
75
79
  };
76
80
 
81
+ /**
82
+ * Validates the output of the satellite.js propagate function.
83
+ * The satellite.js propagate function returns a object with the following properties:
84
+ * - position: {x: number, y: number, z: number}
85
+ * - velocity: {x: number, y: number, z: number}
86
+ * - meanElements: {am: number, em: number, im: number, Om: number, om: number, nm: number, mm: number}
87
+ *
88
+ * When propgation fails, the output is null. However, there are cases where the output is not null, but the position and velocity are NaN.
89
+ * This happens when the input satrec is invalid or malformed due to a bad TLE, in some way. This is rare, and hard to reproduce.
90
+ *
91
+ * This function takes into account those rare cases as part of the validation.
92
+ *
93
+ * @param {Object} out The output of the satellite.js propagate function.
94
+ * @return {boolean} true if the propagation output is valid, false otherwise
95
+ */
96
+ const isPropagateValid = (out) => {
97
+ if (out === null || out === undefined) {
98
+ return false;
99
+ }
100
+
101
+ if (out.position) {
102
+ const pos = out.position;
103
+ if (pos.x === null || pos.x === undefined || Number.isNaN(pos.x)
104
+ || pos.y === null || pos.y === undefined || Number.isNaN(pos.y)
105
+ || pos.z === null || pos.z === undefined || Number.isNaN(pos.z)) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ if (out.velocity) {
111
+ const vel = out.velocity;
112
+ if (vel.x === null || vel.x === undefined || Number.isNaN(vel.x)
113
+ || vel.y === null || vel.y === undefined || Number.isNaN(vel.y)
114
+ || vel.z === null || vel.z === undefined || Number.isNaN(vel.z)) {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ return true;
120
+ };
121
+
122
+ /** A function that attempts to propagate a satellite record to a given time, and returns a standardized output object.
123
+ *
124
+ * @param {Object} satrec The satellite record from satellite.js
125
+ * @param {Date} time The time to propagate the satellite to
126
+ * @return {Object} An object with the following properties:
127
+ * - ok: 0 or 1
128
+ * - err: null or an error message
129
+ * - out: null or the output of the satellite.js propagate function
130
+ */
131
+ const tryPropagateSatrec = (satrec, time) => {
132
+ // Validate time
133
+ if (!time || !(time instanceof Date)) {
134
+ return {
135
+ ok: 0,
136
+ err: ERROR_CODES.INVALID_TIME_INPUT,
137
+ out: null,
138
+ };
139
+ }
140
+ // Validate satrec
141
+ if (!satrec) {
142
+ return {
143
+ ok: 0,
144
+ err: ERROR_CODES.INVALID_SATELLITE_RECORD,
145
+ out: null,
146
+ };
147
+ }
148
+ // Propagate
149
+ try {
150
+ const out = propagate(satrec, time);
151
+ // Validate propagate output
152
+ if (!isPropagateValid(out)) {
153
+ return {
154
+ ok: 0,
155
+ err: ERROR_CODES.INVALID_PROPAGATE_OUTPUT + `: ${satrec.error}`,
156
+ out: null,
157
+ };
158
+ }
159
+ return {
160
+ ok: 1,
161
+ err: null,
162
+ out: out,
163
+ };
164
+ } catch (e) {
165
+ return {
166
+ ok: 0,
167
+ err: ERROR_CODES.INVALID_PROPAGATE_OUTPUT + `: ${e.message}`,
168
+ out: null,
169
+ };
170
+ }
171
+ };
172
+
77
173
  /**
78
174
  * Calculates the semi-major axis in kilometers from a satellite record.
79
175
  * @param {Object} satrec The satellite record from satellite.js
@@ -361,25 +457,19 @@ const angleBetween3DCoords = (coord1, coord2, coord3) => {
361
457
  * which uses the In-track,Cross-Track axes to instantiate it.
362
458
  * @param {Object} pv1
363
459
  * @param {Object} pv2
364
- * @return {Object} Delta-v in m/s in RSW frame to perform a single impulsive
460
+ * @return {Object} Delta-v in km/s in RSW frame to perform a single impulsive
365
461
  * plane match maneuver of sat1 to sat2.
366
462
  */
367
463
  const planeChangeDeltaV = (pv1, pv2) => {
368
- const mu = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
369
-
370
464
  // 1. Compute the angular momentum of each orbit from the pos vel vectors
371
- const r = multiply([pv1.position.x, pv1.position.y, pv1.position.z],
372
- 1000.0);
373
- const v = multiply([pv1.velocity.x, pv1.velocity.y, pv1.velocity.z],
374
- 1000.0);
465
+ const r = [pv1.position.x, pv1.position.y, pv1.position.z];
466
+ const v = [pv1.velocity.x, pv1.velocity.y, pv1.velocity.z];
375
467
  const h1 = cross(r, v);
376
468
 
377
469
  const vMag = norm(v);
378
470
 
379
- const r2 = multiply([pv2.position.x, pv2.position.y, pv2.position.z],
380
- 1000.0);
381
- const v2 = multiply([pv2.velocity.x, pv2.velocity.y, pv2.velocity.z],
382
- 1000.0);
471
+ const r2 = [pv2.position.x, pv2.position.y, pv2.position.z];
472
+ const v2 = [pv2.velocity.x, pv2.velocity.y, pv2.velocity.z];
383
473
  const h2 = cross(r2, v2);
384
474
 
385
475
  // 2. Compute the mutual line of nodes vector
@@ -387,9 +477,9 @@ const planeChangeDeltaV = (pv1, pv2) => {
387
477
 
388
478
  // 3. Compute the eccentricity vector of sat1
389
479
  const e = multiply(
390
- (1 / mu),
480
+ (1 / MU),
391
481
  subtract(
392
- (multiply(Math.pow(norm(v, 2), 2) - (mu / norm(r, 2)), r)),
482
+ (multiply(Math.pow(norm(v, 2), 2) - (MU / norm(r, 2)), r)),
393
483
  multiply(dot(r, v), v)),
394
484
  );
395
485
 
@@ -480,21 +570,21 @@ const planeChangeDeltaV = (pv1, pv2) => {
480
570
  *
481
571
  * @param {Object} pv1 Position and Velocity Vector of Satelltie 1 at Time = x
482
572
  * @param {Object} pv2 Position and velocity Vector of Satellite 2 at Time = x
483
- * @return {Object} Delta-v in m/s to perform a pure inclination plane change at
573
+ * @return {Object} Delta-v in km/s to perform a pure inclination plane change at
484
574
  * the sat1 asc or desc node.
485
575
  */
486
576
  const planeChangePureInclinationDeltaV = (pv1, pv2)=>{
487
577
  // 1. Get position and velocity vectors and magnitudes.
488
578
  // Note that the magnitude of the final velocity for sat1 will be EQUAL
489
579
  // to this initial velocity magnitude of sat1!
490
- const r = multiply([pv1.position.x, pv1.position.y, pv1.position.z], 1000.0);
491
- const v = multiply([pv1.velocity.x, pv1.velocity.y, pv1.velocity.z], 1000.0);
580
+ const r = [pv1.position.x, pv1.position.y, pv1.position.z];
581
+ const v = [pv1.velocity.x, pv1.velocity.y, pv1.velocity.z];
492
582
 
493
- // Velocity Magnitude in m/s
583
+ // Velocity Magnitude in km/s
494
584
  const vMag = norm(v);
495
585
 
496
- const r2 = multiply([pv2.position.x, pv2.position.y, pv2.position.z], 1000.0);
497
- const v2 = multiply([pv2.velocity.x, pv2.velocity.y, pv2.velocity.z], 1000.0);
586
+ const r2 = [pv2.position.x, pv2.position.y, pv2.position.z];
587
+ const v2 = [pv2.velocity.x, pv2.velocity.y, pv2.velocity.z];
498
588
 
499
589
  // 2. Compute the Keplerian Elements of both satellites.
500
590
  const el = cartesianToKeplerian(r, v);
@@ -1311,12 +1401,12 @@ const GetElsetUdlFromTle = (
1311
1401
  * @param {Number} lon1 The longitude of the first coordinate in degrees
1312
1402
  * @param {Number} lat2 The latitude of the second coordinate in degrees
1313
1403
  * @param {Number} lon2 The longitude of the second coordinate in degrees
1314
- * @return {Number} The distance between the two coordinates in meters
1404
+ * @return {Number} The distance between the two coordinates in kilometers
1315
1405
  *
1316
1406
  * Source: https://www.movable-type.co.uk/scripts/latlong.html
1317
1407
  */
1318
1408
  const distGeodetic = (lat1, lon1, lat2, lon2) => {
1319
- const R = WGS84_EARTH_EQUATORIAL_RADIUS_KM * 1000.0; // metres
1409
+ const R = WGS84_EARTH_EQUATORIAL_RADIUS_KM; // kilometers
1320
1410
  const phi1 = lat1 * DEG2RAD; // φ1 in formula
1321
1411
  const phi2 = lat2 * DEG2RAD; // φ2 in formula
1322
1412
  const deltaPhi = (lat2 - lat1) * DEG2RAD; // Δφ in formula
@@ -1329,7 +1419,7 @@ const distGeodetic = (lat1, lon1, lat2, lon2) => {
1329
1419
  * Math.sin(deltaLambda / 2);
1330
1420
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1331
1421
 
1332
- const d = R * c; // in metres
1422
+ const d = R * c; // in kilometers
1333
1423
 
1334
1424
  return d;
1335
1425
  };
@@ -1342,13 +1432,13 @@ const distGeodetic = (lat1, lon1, lat2, lon2) => {
1342
1432
  * NOTE 2: raan, argument of periapsis, and true anomaly are ALL in degrees!
1343
1433
  * NOTE 3: SMA (a) is in km
1344
1434
  *
1345
- * @param {Array} r Position, in meters
1346
- * @param {Array} v Velocity, in m/s
1435
+ * @param {Array} r Position, in kilometers
1436
+ * @param {Array} v Velocity, in km/s
1437
+ * @param {Number} mu Gravitational Parameter, defaults to Earth's mu in km^3/s^2
1347
1438
  * @return {Object} An object containing the Keplerian Elements, if successful. Otherwise, an empty object.
1348
1439
  */
1349
- const cartesianToKeplerian = (r, v) => {
1440
+ const cartesianToKeplerian = (r, v, mu = MU) => {
1350
1441
  try {
1351
- const mu = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
1352
1442
  const tol = 1e-9;
1353
1443
 
1354
1444
  const h = cross(r, v);
@@ -1359,20 +1449,20 @@ const cartesianToKeplerian = (r, v) => {
1359
1449
  if (norm(v, 2) === 0) throw new Error("Velocity vector must not be zero.");
1360
1450
 
1361
1451
  const e = multiply(
1362
- (1 / mu),
1452
+ (1 / MU),
1363
1453
  subtract(
1364
- (multiply(Math.pow(norm(v, 2), 2) - (mu / norm(r, 2)), r)),
1454
+ (multiply(Math.pow(norm(v, 2), 2) - (MU / norm(r, 2)), r)),
1365
1455
  multiply(dot(r, v), v)),
1366
1456
  );
1367
1457
 
1368
- const zeta = 0.5 * Math.pow(norm(v), 2) - (mu / norm(r));
1458
+ const zeta = 0.5 * Math.pow(norm(v), 2) - (MU / norm(r));
1369
1459
 
1370
1460
  if (zeta === 0) throw new Error("Zeta cannot be zero.");
1371
1461
  if (Math.abs(1.0 - norm(e)) <= tol) {
1372
1462
  throw new Error("Parabolic orbit conversion is not supported.");
1373
1463
  }
1374
1464
 
1375
- const a = -mu / zeta / 2;
1465
+ const a = -MU / zeta / 2;
1376
1466
 
1377
1467
  if (Math.abs(a * (1 - norm(e))) < 1e-3) {
1378
1468
  throw new Error(`The state results in a singular conic section with
@@ -1462,7 +1552,7 @@ const cartesianToKeplerian = (r, v) => {
1462
1552
  }
1463
1553
 
1464
1554
  return {
1465
- a: a /1000.0, // km
1555
+ a: a, // km
1466
1556
  e: norm(e),
1467
1557
  i: i * RAD2DEG, // deg
1468
1558
  raan: raan * RAD2DEG, // deg
@@ -1485,9 +1575,9 @@ const cartesianToKeplerian = (r, v) => {
1485
1575
  *
1486
1576
  * @param {Array} elset Keplerian elements
1487
1577
  * @param {Number} mu The gravitational parameter for the celestial object that we orbit, in km based units
1488
- * @return {Object} An object containing the position and velocity in *meter* based units
1578
+ * @return {Object} An object containing the position and velocity in *kilometer* based units
1489
1579
  */
1490
- const keplerianToCartesian = (elset, mu = 398600.4418) => {
1580
+ const keplerianToCartesian = (elset, mu = MU) => {
1491
1581
  const INFINITE_TOL = 1e-10;
1492
1582
  const ORBIT_TOL = 1e-10;
1493
1583
  try {
@@ -1523,17 +1613,17 @@ const keplerianToCartesian = (elset, mu = 398600.4418) => {
1523
1613
  const sinPer = Math.sin(w);
1524
1614
 
1525
1615
  const r = {
1526
- x: 1000.0 * rad * (cosPerAnom * cosRaan - cosInc * sinPerAnom * sinRaan),
1527
- y: 1000.0 * rad * (cosPerAnom * sinRaan + cosInc * sinPerAnom * cosRaan),
1528
- z: 1000.0 * rad * sinPerAnom * sinInc,
1616
+ x: rad * (cosPerAnom * cosRaan - cosInc * sinPerAnom * sinRaan),
1617
+ y: rad * (cosPerAnom * sinRaan + cosInc * sinPerAnom * cosRaan),
1618
+ z: rad * sinPerAnom * sinInc,
1529
1619
  };
1530
1620
 
1531
1621
  const v = {
1532
- x: 1000.0 * (sqrtGravP * cosAnomPlusE * (-sinPer * cosRaan - cosInc * sinRaan * cosPer)
1533
- - sqrtGravP * sinAnom * (cosPer * cosRaan - cosInc * sinRaan * sinPer)),
1534
- y: 1000.0 * (sqrtGravP * cosAnomPlusE * (-sinPer * sinRaan + cosInc * cosRaan * cosPer)
1535
- - sqrtGravP * sinAnom * (cosPer * sinRaan + cosInc * cosRaan * sinPer)),
1536
- z: 1000.0 * (sqrtGravP * (cosAnomPlusE * sinInc * cosPer - sinAnom * sinInc * sinPer)),
1622
+ x: sqrtGravP * cosAnomPlusE * (-sinPer * cosRaan - cosInc * sinRaan * cosPer)
1623
+ - sqrtGravP * sinAnom * (cosPer * cosRaan - cosInc * sinRaan * sinPer),
1624
+ y: sqrtGravP * cosAnomPlusE * (-sinPer * sinRaan + cosInc * cosRaan * cosPer)
1625
+ - sqrtGravP * sinAnom * (cosPer * sinRaan + cosInc * cosRaan * sinPer),
1626
+ z: sqrtGravP * (cosAnomPlusE * sinInc * cosPer - sinAnom * sinInc * sinPer),
1537
1627
  };
1538
1628
 
1539
1629
  return {r, v};
@@ -1545,8 +1635,8 @@ const keplerianToCartesian = (elset, mu = 398600.4418) => {
1545
1635
  const cartesianToElsetElements = (pv, epoch) => {
1546
1636
  // sma, eccentricity, inclination, raan, argp, trueAnomaly
1547
1637
  const kepl = cartesianToKeplerian(
1548
- multiply(posToArray(pv.position), 1000.0), // km to m
1549
- multiply(posToArray(pv.velocity), 1000.0), // km/s to m/s
1638
+ posToArray(pv.position), // km
1639
+ posToArray(pv.velocity), // km/s
1550
1640
  );
1551
1641
 
1552
1642
  const elset = {
@@ -1558,8 +1648,7 @@ const cartesianToElsetElements = (pv, epoch) => {
1558
1648
  };
1559
1649
 
1560
1650
  // Mean motion in radians per second
1561
- const mu = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
1562
- const meanMotion = Math.sqrt(mu/Math.pow((elset.SemiMajorAxis*1000.0), 3));
1651
+ const meanMotion = Math.sqrt(MU/Math.pow((elset.SemiMajorAxis), 3));
1563
1652
 
1564
1653
  elset.MeanMotion = meanMotion / (2*Math.PI) * 60 * 60 * 24; // rads/s to revs per day
1565
1654
 
@@ -1654,8 +1743,8 @@ const getLeoRpoData = (line1, line2, sats, startTime, endTime) => {
1654
1743
  aResult.di = angleBetweenPlanes(pv1, pv2);
1655
1744
  aResult.dv = planeChangeDeltaV(pv1, pv2);
1656
1745
  aResult.dv = {
1657
- i: Math.round(aResult.dv.i*1000)/1000,
1658
- c: Math.round(aResult.dv.c*1000)/1000,
1746
+ i: Math.round(aResult.dv.i*1000000)/1000, // Confirm rounding
1747
+ c: Math.round(aResult.dv.c*1000000)/1000,
1659
1748
  }; // Round to 3 decimals
1660
1749
 
1661
1750
  // Find the distance at each time step
@@ -1768,8 +1857,8 @@ const getGeoRpoData = (line1, line2, sats, startTime, endTime, lonTime) => {
1768
1857
  };
1769
1858
  aResult.di = angleBetweenPlanes(pv1, pv2);
1770
1859
  aResult.dv = {
1771
- i: Math.round(planeChangeDeltaV(pv1, pv2).i*1000)/1000,
1772
- c: Math.round(planeChangeDeltaV(pv1, pv2).c*1000)/1000,
1860
+ i: Math.round(planeChangeDeltaV(pv1, pv2).i*1000000)/1000, // Confirm rounding
1861
+ c: Math.round(planeChangeDeltaV(pv1, pv2).c*1000000)/1000,
1773
1862
  }; // Round to 3 decimals
1774
1863
 
1775
1864
  for (let i=0; i<pEphem.length; i++) {
@@ -2053,10 +2142,8 @@ const calculateGeoCrossingTimes = async (propagateBetween, start, end, stepMs =
2053
2142
  */
2054
2143
  const calculateNextApogeePerigeeTimesWithPropagation
2055
2144
  = async (pv, propagateTo, time, findApogee=true, findPerigee=true) => {
2056
- const r = multiply([pv.position.x, pv.position.y, pv.position.z],
2057
- 1000.0);
2058
- const v = multiply([pv.velocity.x, pv.velocity.y, pv.velocity.z],
2059
- 1000.0);
2145
+ const r = [pv.position.x, pv.position.y, pv.position.z];
2146
+ const v = [pv.velocity.x, pv.velocity.y, pv.velocity.z];
2060
2147
  const el = cartesianToKeplerian(r, v);
2061
2148
 
2062
2149
  // Compute Eccentric Anomaly from True Anomaly and Eccentricity
@@ -2066,8 +2153,7 @@ const calculateNextApogeePerigeeTimesWithPropagation
2066
2153
  const M = E - (el.e)*Math.sin(E);
2067
2154
 
2068
2155
  // Mean motion in radians per second
2069
- const mu = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
2070
- const n = Math.sqrt(mu/Math.pow((el.a*1000.0), 3));
2156
+ const n = Math.sqrt(MU/Math.pow((el.a), 3));
2071
2157
 
2072
2158
  // Orbit Period
2073
2159
  const periodSecs = 2*Math.PI/n;
@@ -2324,8 +2410,8 @@ const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
2324
2410
  const ephem = prop(elset, segmentStart, segmentEnd, stepMs);
2325
2411
  satEphems[satIndex].push(...ephem.map((point, pointInd) => {
2326
2412
  const osculatingElements = cartesianToKeplerian(
2327
- multiply(posToArray(point.p), 1000.0), // km to m
2328
- multiply(posToArray(point.v), 1000.0), // km/s to m/s
2413
+ posToArray(point.p), // km
2414
+ posToArray(point.v), // km/s
2329
2415
  );
2330
2416
  return {
2331
2417
  ...point,
@@ -2408,9 +2494,7 @@ const getLeoWaterfallData = (elsets, startTime, endTime, stepMs = 10000) => {
2408
2494
  * @param {Date} startTime The start time of the maneuver as unix timestamp in milliseconds
2409
2495
  * @return {Object} An object containing the maneuver properties
2410
2496
  */
2411
- const getInterceptRendezvousMinDv = (sat1Tle, sat2Tle, startTime) =>{
2412
- const mu = 3.986004415e5; // km based
2413
-
2497
+ const getInterceptRendezvousMinDv = (sat1Tle, sat2Tle, startTime) => {
2414
2498
  let minDv = Infinity;
2415
2499
  let revNum = 0;
2416
2500
  let minIntercept = [0, 0, 0]; // The minimum dv to intercept the target.
@@ -2431,7 +2515,7 @@ const getInterceptRendezvousMinDv = (sat1Tle, sat2Tle, startTime) =>{
2431
2515
  const r2 = [sat2Pv.position.x, sat2Pv.position.y, sat2Pv.position.z]; // km based
2432
2516
 
2433
2517
 
2434
- const {v1, v2, vH1} = lambertThomsonAlgorithm(r1, r2, dt, rev, 0, v1Before, mu);
2518
+ const {v1, v2, vH1} = lambertThomsonAlgorithm(r1, r2, dt, rev, 0, v1Before, MU_GRS80);
2435
2519
  const deltaV1 = isDefined(v1) ? subtract(v1, v1Before) : [0, 0, 0];
2436
2520
  const deltaVH1 = isDefined(vH1) ? subtract(vH1, v1Before): [0, 0, 0];
2437
2521
  const v1Mag = norm(deltaV1);
@@ -2511,7 +2595,7 @@ const getInterceptRendezvousMinDv = (sat1Tle, sat2Tle, startTime) =>{
2511
2595
  interceptTime: new Date(startTime + (minDt*1000)).toISOString(),
2512
2596
  interceptManeuverECI_ms: {
2513
2597
  time: new Date(startTime).toISOString(),
2514
- x: minIntercept.x*1000, // km/s to m/s
2598
+ x: minIntercept.x*1000, // km/s to m/s
2515
2599
  y: minIntercept.y*1000,
2516
2600
  z: minIntercept.z*1000,
2517
2601
  },
@@ -2593,15 +2677,12 @@ const detectManeuverMinDv = (initialTLE, finalTLE) => {
2593
2677
  // Position of final TLE at its epoch
2594
2678
  const r2 = [pvFinal.position.x, pvFinal.position.y, pvFinal.position.z]; // km based
2595
2679
 
2596
- // Earth Gravitational Parameter
2597
- const mu = 3.986004415e5; // km based
2598
-
2599
2680
  // Loop for N until NaN
2600
2681
  let minNorm = Infinity;
2601
2682
  // let minIndex = -1;
2602
2683
  let minVector = [0, 0, 0];
2603
2684
  for (let i=0; i<10; i++) { // loop up to 10 revolutions, it is a safe upper limit to capture impulsive maneuvers
2604
- const {v1, vH1} = lambertThomsonAlgorithm(r1, r2, dt, i, 0, v1Before, mu);
2685
+ const {v1, vH1} = lambertThomsonAlgorithm(r1, r2, dt, i, 0, v1Before, MU_GRS80);
2605
2686
 
2606
2687
  const deltaV1 = isDefined(v1) ? subtract(v1, v1Before) : [0, 0, 0];
2607
2688
  const deltaVH1 = isDefined(vH1) ? subtract(vH1, v1Before): [0, 0, 0];
@@ -2663,7 +2744,7 @@ const detectManeuverMinDv = (initialTLE, finalTLE) => {
2663
2744
  *
2664
2745
  */
2665
2746
  const lambertThomsonAlgorithm
2666
- = (r1, r2, t, N, D = 0, v1Minus, mu = 3.986004415e14, outOfPlaneError = 0) => {
2747
+ = (r1, r2, t, N, D = 0, v1Minus, mu = MU_SI, outOfPlaneError = 0) => {
2667
2748
  // Out-Of-Plane error check
2668
2749
  const r1Mag = norm(r1);
2669
2750
  const r2Mag = norm(r2);
@@ -2857,7 +2938,7 @@ const lambertThomsonAlgorithm
2857
2938
  * @param {number} mu Gravitational parameter
2858
2939
  * @return {{v1: Array<number>, v2: Array<number>}} Initial and final velocity vectors
2859
2940
  */
2860
- const hodographVelocityAlgorithm = (r1, r2, t, v1Minus, theta, p, e, mu) => {
2941
+ const hodographVelocityAlgorithm = (r1, r2, t, v1Minus, theta, p, e, mu = MU_SI) => {
2861
2942
  // Line 2: Define L180 (in meters)
2862
2943
  const L180 = 1.0;
2863
2944
 
@@ -3209,6 +3290,7 @@ export {REGIMES,
3209
3290
  getRaanDetails,
3210
3291
  isSatInShadow,
3211
3292
  calculateGeoCrossingTimes,
3293
+ tryPropagateSatrec,
3212
3294
  };
3213
3295
  export const raDecToGeodetic = RaDecToGeodetic;
3214
3296
  export const getResiduals = GetResiduals;
@@ -13,7 +13,7 @@ import {NodeVector3D} from "./NodeVector3D.js";
13
13
  // Earth constants using existing constants.js values
14
14
  const EarthConstants = {
15
15
  EquatorialRadiusKm: WGS84_EARTH_EQUATORIAL_RADIUS_KM, // 6378.137 km
16
- Mu: MU / 1e9, // Convert from m³/s² to km³/s² (398600.4418)
16
+ Mu: MU, // km³/s²
17
17
  J2: 1082.62999e-6,
18
18
  J3: -2.53215e-6,
19
19
  };
package/src/constants.js CHANGED
@@ -7,7 +7,9 @@ export const MILLIS_PER_DAY = 24 * 60 * 60 * 1000; // Number of milliseconds in
7
7
  export const SUN_RADIUS_KM = 695701.0; // Sun radius in kilometers
8
8
  export const AU_KM = 149597870.7; // Astronomical Unit in kilometers
9
9
 
10
- export const MU = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
10
+ export const MU = 3.986004418e5; // km³/s² WGS-84 Earth Mu
11
+ export const MU_GRS80 = 3.986004415e5; // km³/s² Earth Mu in GRS-80
12
+ export const MU_SI = 3.986004415e14; // m³/s² Earth Mu in SI units, GRS-80
11
13
  export const GRAV_CONST = 6.6743e-11; // N⋅m2⋅kg−2
12
14
  export const EARTH_MASS = 5.97219e24; // kg
13
15
  export const WGS72_EARTH_EQUATORIAL_RADIUS_KM = 6378.135; // in km. Use this when calculations are done with SGP4 which uses WGS72 assumptions.
@@ -28,3 +30,8 @@ export const REGIMES = {
28
30
  GeoDrifter: 512,
29
31
  };
30
32
 
33
+ export const ERROR_CODES = {
34
+ INVALID_TIME_INPUT: "Invalid time input",
35
+ INVALID_SATELLITE_RECORD: "Invalid satellite record",
36
+ INVALID_PROPAGATE_OUTPUT: "Invalid propagate output",
37
+ };
@@ -194,7 +194,7 @@ const getOpopOtop = (
194
194
  const opopLat = degreesLat(opop.latitude);
195
195
  const opopLon = degreesLong(opop.longitude);
196
196
 
197
- if (distGeodetic(opopLat, opopLon, padLat, padLon) > opopDistThresholdKm * 1000) {
197
+ if (distGeodetic(opopLat, opopLon, padLat, padLon) > opopDistThresholdKm) {
198
198
  throw new Error(`OPOP is more than ${opopDistThresholdKm}km from launch pad`);
199
199
  }
200
200