@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 +3 -2
- package/src/FrameConverter.js +7 -6
- package/src/LaunchNominalClass.js +57 -78
- package/src/OrbitUtils.js +6 -187
- package/src/PropagateUtils.js +9 -9
- package/src/astro.js +151 -69
- package/src/ballisticPropagator.js +1 -1
- package/src/constants.js +8 -1
- package/src/launchNominal.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saber-usa/node-common",
|
|
3
|
-
"version": "1.7.
|
|
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/**/*"
|
package/src/FrameConverter.js
CHANGED
|
@@ -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.
|
|
961
|
-
const sini = Math.sin(keplerianElements.
|
|
962
|
-
const cosRaan = Math.cos(keplerianElements.raan);
|
|
963
|
-
const sinRaan = Math.sin(keplerianElements.raan);
|
|
964
|
-
const cosw = Math.cos(keplerianElements.
|
|
965
|
-
const sinw = Math.sin(keplerianElements.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
236
|
+
const ellipticalStateVector = keplerianToCartesian(ellipticalElements, mu);
|
|
252
237
|
|
|
253
|
-
const ellipticalPos = ellipticalStateVector.
|
|
254
|
-
const ellipticalVel = ellipticalStateVector.
|
|
238
|
+
const ellipticalPos = ellipticalStateVector.r;
|
|
239
|
+
const ellipticalVel = ellipticalStateVector.v;
|
|
255
240
|
|
|
256
241
|
const ellipticalState = {
|
|
257
|
-
position: new NodeVector3D(ellipticalPos
|
|
258
|
-
velocity: new NodeVector3D(ellipticalVel
|
|
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 =
|
|
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
|
|
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 =
|
|
477
|
-
const incDiff = elements0.
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
574
|
-
|
|
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
|
|
577
|
-
const
|
|
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
|
|
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(
|
|
584
|
+
const mutualNode = this.#findIntersectionEllipseLine(
|
|
606
585
|
slopeMutualNodalLinePerifocal,
|
|
607
|
-
orbit1.
|
|
608
|
-
orbit1.
|
|
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.
|
|
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.
|
|
624
|
-
let e1 = OrbitUtils.trueAnomalyToEccentricAnomaly(n1, orbit1.
|
|
625
|
-
let e2 = OrbitUtils.trueAnomalyToEccentricAnomaly(n2, orbit1.
|
|
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.
|
|
639
|
-
* (2 * k1 * Math.PI + (e1 - orbit1.
|
|
640
|
-
- (e0 - orbit1.
|
|
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.
|
|
643
|
-
* (2 * k2 * Math.PI + (e2 - orbit1.
|
|
644
|
-
- (e0 - orbit1.
|
|
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 =
|
|
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.
|
|
737
|
-
ecc: elements.
|
|
738
|
-
inc: elements.
|
|
715
|
+
sma: elements.a,
|
|
716
|
+
ecc: elements.e,
|
|
717
|
+
inc: elements.i,
|
|
739
718
|
raan: elements.raan,
|
|
740
|
-
argp: elements.
|
|
741
|
-
ta: elements.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/PropagateUtils.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
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 =
|
|
372
|
-
|
|
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 =
|
|
380
|
-
|
|
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 /
|
|
480
|
+
(1 / MU),
|
|
391
481
|
subtract(
|
|
392
|
-
(multiply(Math.pow(norm(v, 2), 2) - (
|
|
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
|
|
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 =
|
|
491
|
-
const v =
|
|
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
|
|
583
|
+
// Velocity Magnitude in km/s
|
|
494
584
|
const vMag = norm(v);
|
|
495
585
|
|
|
496
|
-
const r2 =
|
|
497
|
-
const v2 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1346
|
-
* @param {Array} v Velocity, in
|
|
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 /
|
|
1452
|
+
(1 / MU),
|
|
1363
1453
|
subtract(
|
|
1364
|
-
(multiply(Math.pow(norm(v, 2), 2) - (
|
|
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) - (
|
|
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 = -
|
|
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
|
|
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 *
|
|
1578
|
+
* @return {Object} An object containing the position and velocity in *kilometer* based units
|
|
1489
1579
|
*/
|
|
1490
|
-
const keplerianToCartesian = (elset, mu =
|
|
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:
|
|
1527
|
-
y:
|
|
1528
|
-
z:
|
|
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:
|
|
1533
|
-
- sqrtGravP * sinAnom * (cosPer * cosRaan - cosInc * sinRaan * sinPer)
|
|
1534
|
-
y:
|
|
1535
|
-
- sqrtGravP * sinAnom * (cosPer * sinRaan + cosInc * cosRaan * sinPer)
|
|
1536
|
-
z:
|
|
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
|
-
|
|
1549
|
-
|
|
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
|
|
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*
|
|
1658
|
-
c: Math.round(aResult.dv.c*
|
|
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*
|
|
1772
|
-
c: Math.round(planeChangeDeltaV(pv1, pv2).c*
|
|
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 =
|
|
2057
|
-
|
|
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
|
|
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
|
-
|
|
2328
|
-
|
|
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,
|
|
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, //
|
|
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,
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
+
};
|
package/src/launchNominal.js
CHANGED
|
@@ -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
|
|
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
|
|