@saber-usa/node-common 1.6.207

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.
@@ -0,0 +1,822 @@
1
+ const {degreesToRadians, radiansToDegrees} = require("satellite.js");
2
+ const {Vector3D, J2000, EpochUTC} = require("pious-squid");
3
+ const {NodeVector3D} = require("./NodeVector3D.js");
4
+ const constants = require("./constants.js");
5
+ const {FrameConverter} = require("./FrameConverter.js");
6
+ const {OrbitUtils} = require("./OrbitUtils.js");
7
+ const {wrapOneRevUnsigned} = require("./utils.js");
8
+ const {BallisticPropagator} = require("./ballisticPropagator/ballisticPropagator.js");
9
+ const {norm, cross, dot} = require("mathjs");
10
+
11
+ // Reference frames enum
12
+ const ReferenceFrame = {
13
+ ITRF: "ITRF",
14
+ J2000: "J2000",
15
+ TEME: "TEME",
16
+ GCRF: "GCRF",
17
+ };
18
+
19
+ class LaunchNominalClass {
20
+ /**
21
+ * Helper method to transform a vector using a Matrix3D
22
+ * @param {Vector3D} vector - Vector to transform
23
+ * @param {Matrix3D} matrix - Matrix to use for transformation
24
+ * @return {Vector3D} Transformed vector
25
+ */
26
+ static transformVector(vector, matrix) {
27
+ // Manual matrix-vector multiplication since pious-squid has issues
28
+ const x = matrix.get(0, 0) * vector.x
29
+ + matrix.get(0, 1) * vector.y + matrix.get(0, 2) * vector.z;
30
+ const y = matrix.get(1, 0) * vector.x
31
+ + matrix.get(1, 1) * vector.y + matrix.get(1, 2) * vector.z;
32
+ const z = matrix.get(2, 0) * vector.x
33
+ + matrix.get(2, 1) * vector.y + matrix.get(2, 2) * vector.z;
34
+ return new Vector3D(x, y, z);
35
+ }
36
+
37
+ /**
38
+ * Rotate a vector around an axis by an angle (Rodrigues' rotation formula)
39
+ * @param {Vector3D} vector - Vector to rotate
40
+ * @param {Vector3D} axis - Axis to rotate around (should be normalized)
41
+ * @param {number} angleRad - Angle in radians
42
+ * @return {Vector3D} Rotated vector
43
+ */
44
+ static rotateVector(vector, axis, angleRad) {
45
+ // Ensure axis is normalized
46
+ const k = axis.normalized();
47
+ const v = vector;
48
+ const cosTheta = Math.cos(angleRad);
49
+ const sinTheta = Math.sin(angleRad);
50
+
51
+ // Rodrigues' rotation formula:
52
+ // v_rot = v*cos(θ) + (k × v)*sin(θ) + k*(k·v)*(1-cos(θ))
53
+
54
+ // k × v (cross product)
55
+ const kCrossV = new Vector3D(
56
+ k.y * v.z - k.z * v.y,
57
+ k.z * v.x - k.x * v.z,
58
+ k.x * v.y - k.y * v.x,
59
+ );
60
+
61
+ // k·v (dot product)
62
+ const kDotV = k.x * v.x + k.y * v.y + k.z * v.z;
63
+
64
+ // Final rotation
65
+ return new Vector3D(
66
+ v.x * cosTheta + kCrossV.x * sinTheta + k.x * kDotV * (1 - cosTheta),
67
+ v.y * cosTheta + kCrossV.y * sinTheta + k.y * kDotV * (1 - cosTheta),
68
+ v.z * cosTheta + kCrossV.z * sinTheta + k.z * kDotV * (1 - cosTheta),
69
+ );
70
+ }
71
+
72
+ /** Get position at a given time in specified reference frame
73
+ *
74
+ * @param {Date} utc - UTC time
75
+ * @param {Vector3D} posITRF Position in ITRF frame
76
+ * @return {Vector3D} Position in J2000 frame
77
+ */
78
+ static getPositionJ2000(utc, posITRF) {
79
+ const transformMatrix = FrameConverter.itrfToJ2000(utc);
80
+
81
+ return this.transformVector(posITRF, transformMatrix);
82
+ }
83
+
84
+ /** Generate a circular launch nominal.
85
+ *
86
+ * @param {number} lat - The latitude of the ground site (degrees)
87
+ * @param {number} lon - The longitude of the ground site (degrees)
88
+ * @param {number} alt - The altitude of the ground site (km)
89
+ * @param {Date} T0 - The Liftoff Time
90
+ * @param {number} bearingDeg - The bearing the launch vehicle headed in (degrees)
91
+ * @param {number} leoAltKm - The LEO Parking Altitude (km)
92
+ * @param {number} flyoutTimeSecs - How long the launch vehicle takes to reach the leoAltKm (seconds)
93
+ * @return {Object} Satellite object
94
+ */
95
+ static generateLaunchNominalCircular(lat, lon, alt, T0, bearingDeg, leoAltKm, flyoutTimeSecs) {
96
+ const llaVals = {
97
+ Latitude: {Degrees: lat, Radians: degreesToRadians(lat)},
98
+ Longitude: {Degrees: lon, Radians: degreesToRadians(lon)},
99
+ AltitudeKm: alt,
100
+ };
101
+
102
+ let bearingRad = degreesToRadians(bearingDeg);
103
+ bearingRad = wrapOneRevUnsigned(bearingRad);
104
+
105
+ // const latRad = degreesToRadians(this.groundSiteLat);
106
+ const latRad = llaVals.Latitude.Radians;
107
+ const targetInclination = OrbitUtils.getInclination(bearingRad, latRad);
108
+
109
+ // Convergence parameters
110
+ const inclinationThreshold = 0.001;
111
+ let inclinationDelta = Number.POSITIVE_INFINITY;
112
+ let oldInclinationDelta;
113
+ let stepSize = 0.1; // Initialize step size
114
+ const maxIter = 100;
115
+
116
+ // Time at orbit insertion
117
+ const orbitInsertionTime = new Date(T0.getTime() + flyoutTimeSecs * 1000);
118
+
119
+ const initialStateVector = this.generateStateOverGroundsite(
120
+ orbitInsertionTime,
121
+ radiansToDegrees(bearingRad),
122
+ leoAltKm,
123
+ ReferenceFrame.J2000,
124
+ llaVals,
125
+ );
126
+ // Trying something here
127
+
128
+ // const transformMatrix = FrameConverter.j2000ToTEME(orbitInsertionTime);
129
+ // const initialTemeState = {
130
+ // position: this.transformVector(
131
+ // new Vector3D(initialStateVector.position[0],
132
+ // initialStateVector.position[1], initialStateVector.position[2]),
133
+ // transformMatrix
134
+ // ),
135
+ // velocity: this.transformVector(
136
+ // new Vector3D(initialStateVector.velocity[0],
137
+ // initialStateVector.velocity[1], initialStateVector.velocity[2]),
138
+ // transformMatrix
139
+ // ),
140
+ // };
141
+
142
+ // End of something
143
+ const initialJ2000State = new J2000(
144
+ EpochUTC.fromDateString(orbitInsertionTime.toISOString()),
145
+ new Vector3D(initialStateVector.position[0],
146
+ initialStateVector.position[1], initialStateVector.position[2]),
147
+ new Vector3D(initialStateVector.velocity[0],
148
+ initialStateVector.velocity[1], initialStateVector.velocity[2]),
149
+ );
150
+
151
+ const initialTemeState = initialJ2000State.toTEME();
152
+
153
+ let sat = {
154
+ stateVector: initialStateVector,
155
+ elements: OrbitUtils.stateVectorToElements(
156
+ [initialTemeState.position.x, initialTemeState.position.y,
157
+ initialTemeState.position.z],
158
+ [initialTemeState.velocity.x, initialTemeState.velocity.y,
159
+ initialTemeState.velocity.z]),
160
+ epoch: orbitInsertionTime,
161
+ };
162
+
163
+ // Create initial propagator
164
+ let propagator = null;
165
+
166
+ for (let i = 1; i <= maxIter; i++) {
167
+ if (inclinationDelta > inclinationThreshold) {
168
+ // Generate state vector at orbit insertion
169
+ const sv = this.generateStateOverGroundsite(
170
+ orbitInsertionTime,
171
+ radiansToDegrees(bearingRad),
172
+ leoAltKm,
173
+ ReferenceFrame.J2000,
174
+ llaVals,
175
+ );
176
+
177
+ // Create J2000 state for pious-squid
178
+ const state = {
179
+ position: new NodeVector3D(sv.position[0], sv.position[1], sv.position[2]), // km
180
+ velocity: new NodeVector3D(sv.velocity[0], sv.velocity[1], sv.velocity[2]), // km/s
181
+ epochUtc: orbitInsertionTime,
182
+ referenceFrame: "J2000",
183
+ };
184
+
185
+ // Create propagator
186
+ propagator = new BallisticPropagator(state);
187
+
188
+ // Propagate to T0 to get the state at liftoff
189
+ const propagated = propagator.propagate(T0);
190
+
191
+ const propagatedJ2000State = new J2000(
192
+ EpochUTC.fromDateString(T0.toISOString()),
193
+ new Vector3D(propagated.position.x, propagated.position.y,
194
+ propagated.position.z),
195
+ new Vector3D(propagated.velocity.x, propagated.velocity.y,
196
+ propagated.velocity.z),
197
+ );
198
+
199
+ const temeState = propagatedJ2000State.toTEME();
200
+
201
+ // Convert to orbital elements
202
+ const elements = OrbitUtils.stateVectorToElements(
203
+ [temeState.position.x, temeState.position.y,
204
+ temeState.position.z],
205
+ [temeState.velocity.x, temeState.velocity.y,
206
+ temeState.velocity.z]);
207
+
208
+ sat = {
209
+ stateVector: propagated,
210
+ elements: elements,
211
+ epoch: T0,
212
+ };
213
+
214
+ // Calculate current inclination
215
+ oldInclinationDelta = inclinationDelta;
216
+ inclinationDelta = Math.abs(radiansToDegrees(sat.elements.inclination)
217
+ - radiansToDegrees(targetInclination));
218
+
219
+ if (oldInclinationDelta < inclinationDelta) {
220
+ // If getting farther away, reverse direction and make smaller steps
221
+ stepSize *= -0.5;
222
+ }
223
+
224
+ // Update bearing
225
+ bearingRad = wrapOneRevUnsigned(bearingRad + degreesToRadians(stepSize));
226
+ } else {
227
+ break;
228
+ }
229
+
230
+ if (i === maxIter) {
231
+ throw new Error(
232
+ "Launch Nominal inclination did not converge within max iterations");
233
+ }
234
+ }
235
+
236
+ const out = new LaunchNominalOutput("CIRCULAR")
237
+ .addState(sat.stateVector, sat.elements, sat.epoch);
238
+ return out.toJSON();
239
+ }
240
+
241
+
242
+ /** Generate an elliptical launch nominal.
243
+ *
244
+ * @param {number} lat - The latitude of the ground site (degrees)
245
+ * @param {number} lon - The longitude of the ground site (degrees)
246
+ * @param {number} alt - The altitude of the ground site (km)
247
+ * @param {Date} T0 - The Liftoff Time
248
+ * @param {number} bearingDeg - The bearing the launch vehicle headed in (degrees)
249
+ * @param {number} flyoutTimeSecs - How long the launch vehicle takes to reach orbit (seconds)
250
+ * @param {number} targetEcc - Target Eccentricity
251
+ * @param {number} targetArgOfPeriapsisDeg - Target Argument of Periapsis (degrees)
252
+ * @param {number} targetPerigeeKm - Target Perigee (km)
253
+ * @return {Object} Satellite object
254
+ */
255
+ static generateLaunchNominalElliptical(lat, lon, alt, T0, bearingDeg, flyoutTimeSecs,
256
+ targetEcc, targetArgOfPeriapsisDeg, targetPerigeeKm) {
257
+ // First get circular nominal
258
+ const initialNominal = this.generateLaunchNominalCircular(lat, lon, alt, T0,
259
+ bearingDeg, targetPerigeeKm, flyoutTimeSecs);
260
+
261
+ // Time at orbit insertion
262
+ const orbitInsertionTime = new Date(T0.getTime() + flyoutTimeSecs * 1000);
263
+
264
+ // Create J2000 state from initialNominal state vector
265
+ const initialNominalPos = initialNominal.states[0].sv.r;
266
+ const initialNominalVel = initialNominal.states[0].sv.v;
267
+
268
+ const state = {
269
+ position: new NodeVector3D(
270
+ initialNominalPos[0], initialNominalPos[1], initialNominalPos[2]),
271
+ velocity: new NodeVector3D(
272
+ initialNominalVel[0], initialNominalVel[1], initialNominalVel[2]),
273
+ epochUtc: T0,
274
+ referenceFrame: "J2000",
275
+ };
276
+
277
+ // Propagate state to orbit insertion time
278
+ const propagator = new BallisticPropagator(state);
279
+ const propagated = propagator.propagate(orbitInsertionTime);
280
+ const svInitial = {
281
+ position: [propagated.position.x, propagated.position.y, propagated.position.z],
282
+ velocity: [propagated.velocity.x, propagated.velocity.y, propagated.velocity.z],
283
+ epoch: orbitInsertionTime,
284
+ };
285
+
286
+ // Transform to elliptical orbit
287
+ const ellipticalElements = OrbitUtils.transformElliptical(
288
+ svInitial,
289
+ initialNominal.states[0].elements.inc,
290
+ initialNominal.states[0].elements.raan,
291
+ targetEcc,
292
+ degreesToRadians(targetArgOfPeriapsisDeg),
293
+ targetPerigeeKm + constants.WGS84_EARTH_EQUATORIAL_RADIUS_KM,
294
+ );
295
+
296
+ // Convert elliptical elements to cartesian state
297
+ const ellipticalStateVector = OrbitUtils.elementsToStateVector(ellipticalElements);
298
+
299
+ const ellipticalPos = ellipticalStateVector.position;
300
+ const ellipticalVel = ellipticalStateVector.velocity;
301
+
302
+ const ellipticalState = {
303
+ position: new NodeVector3D(ellipticalPos[0], ellipticalPos[1], ellipticalPos[2]),
304
+ velocity: new NodeVector3D(ellipticalVel[0], ellipticalVel[1], ellipticalVel[2]),
305
+ epochUtc: orbitInsertionTime,
306
+ referenceFrame: "J2000",
307
+ };
308
+
309
+ // Propagate state to T0 (back propagate)
310
+ const propagator2 = new BallisticPropagator(ellipticalState);
311
+ const ellipticalNominal = propagator2.propagate(T0);
312
+
313
+ const ellipNomPos = ellipticalNominal.position;
314
+ const ellipNomVel = ellipticalNominal.velocity;
315
+
316
+ // Convert to orbital elements
317
+ const elements = OrbitUtils.stateVectorToElements(
318
+ [ellipNomPos.x, ellipNomPos.y, ellipNomPos.z],
319
+ [ellipNomVel.x, ellipNomVel.y, ellipNomVel.z]);
320
+
321
+ // Return satellite object with elements and propagator
322
+ const out = new LaunchNominalOutput("ELLIPTICAL")
323
+ .addState(ellipticalNominal, elements, T0);
324
+ return out.toJSON();
325
+ }
326
+
327
+ static generateLaunchNominalGtoCircular(
328
+ lat, lon, alt, T0, bearingDeg, leoAltKm, flyoutTimeSecs, burnAtNodes = 1) {
329
+ const circLaunchNom = this.generateLaunchNominalCircular(
330
+ lat, lon, alt, T0, bearingDeg, leoAltKm, flyoutTimeSecs);
331
+ const transfer = this.hohmannTransferWithIncZeroing(
332
+ {
333
+ position: new NodeVector3D(
334
+ circLaunchNom.states[0].sv.r[0],
335
+ circLaunchNom.states[0].sv.r[1],
336
+ circLaunchNom.states[0].sv.r[2],
337
+ ),
338
+ velocity: new NodeVector3D(
339
+ circLaunchNom.states[0].sv.v[0],
340
+ circLaunchNom.states[0].sv.v[1],
341
+ circLaunchNom.states[0].sv.v[2],
342
+ ),
343
+ epochUtc: new Date(circLaunchNom.states[0].epoch),
344
+ },
345
+ 42157.137,
346
+ burnAtNodes);
347
+
348
+ const out = new LaunchNominalOutput("CIRCULAR_GTO");
349
+ out
350
+ .addState(transfer.state0, null, transfer.state0.epochUtc)
351
+ .addBurn(transfer.burn1.time, transfer.burn1.dv)
352
+ .addState(transfer.state1, null, transfer.state1.epochUtc)
353
+ .addBurn(transfer.burn2.time, transfer.burn2.dv)
354
+ .addState(transfer.state2, null, transfer.state2.epochUtc);
355
+ return out.toJSON();
356
+ }
357
+
358
+ static generateLaunchNominalGtoEliptical(
359
+ lat, lon, alt, T0, bearingDeg, flyoutTimeSecs, targetEcc,
360
+ targetAop, targetPerigee, burnAtNodes = 1) {
361
+ const ellipLaunchNom = this.generateLaunchNominalElliptical(
362
+ lat, lon, alt, T0, bearingDeg, flyoutTimeSecs,
363
+ targetEcc, targetAop, targetPerigee);
364
+ const transfer = this.hohmannTransferWithIncZeroing(
365
+ {
366
+ position: new NodeVector3D(
367
+ ellipLaunchNom.states[0].sv.r[0],
368
+ ellipLaunchNom.states[0].sv.r[1],
369
+ ellipLaunchNom.states[0].sv.r[2],
370
+ ),
371
+ velocity: new NodeVector3D(
372
+ ellipLaunchNom.states[0].sv.v[0],
373
+ ellipLaunchNom.states[0].sv.v[1],
374
+ ellipLaunchNom.states[0].sv.v[2],
375
+ ),
376
+ epochUtc: new Date(ellipLaunchNom.states[0].epoch),
377
+ },
378
+ 42157.137,
379
+ burnAtNodes);
380
+ const out = new LaunchNominalOutput("ELLIPTICAL_GTO");
381
+ out
382
+ .addState(transfer.state0, null, transfer.state0.epochUtc)
383
+ .addBurn(transfer.burn1.time, transfer.burn1.dv)
384
+ .addState(transfer.state1, null, transfer.state1.epochUtc)
385
+ .addBurn(transfer.burn2.time, transfer.burn2.dv)
386
+ .addState(transfer.state2, null, transfer.state2.epochUtc);
387
+ return out.toJSON();
388
+ }
389
+
390
+ /**
391
+ *
392
+ * Create the initial state vector of a launch from this GroundSite
393
+ * Matches C# implementation exactly
394
+ * @param {Date} orbitInsertionTime - Orbit Insertion Time: Launch Time + Flyout Time.
395
+ * @param {number} azimuthDeg - Azimuth in degrees
396
+ * @param {number} orbitAltitudeKm - Orbit altitude in km
397
+ * @param {string} frame - Reference frame (default: J2000)
398
+ * @param {Object} llaVals - LLA values of the ground site
399
+ * @return {Object} State vector {position, velocity}
400
+ */
401
+ static generateStateOverGroundsite(
402
+ orbitInsertionTime, azimuthDeg, orbitAltitudeKm, frame = ReferenceFrame.J2000, llaVals) {
403
+ const minAltitudeKm = 1.0; // minimum launch altitude. Lowest recoded is about 440m.
404
+ if (orbitAltitudeKm < minAltitudeKm) {
405
+ throw new Error(`Altitude cannot be less than ${minAltitudeKm} km.`);
406
+ }
407
+
408
+ const azimuthRad = degreesToRadians(azimuthDeg);
409
+ const earthRadius = constants.WGS84_EARTH_EQUATORIAL_RADIUS_KM;
410
+ const mu = constants.GRAV_CONST * constants.EARTH_MASS / 1e9; // km^3/s^2
411
+
412
+ // Get transformation from ITRF to J2000
413
+ const itrf2j2000 = FrameConverter.itrfToJ2000(orbitInsertionTime);
414
+
415
+ // Transform North and Nadir to J2000 - derive unit direction vectors from SEZ -> ITRF/ECEF -> J2000/ECI
416
+
417
+ // Calculate ITRF position using the site
418
+ const positionITRF = FrameConverter.llaToECEF(llaVals);
419
+
420
+ // Get transformation matrices
421
+ const sez2ecef = FrameConverter.sezToECEF(llaVals);
422
+
423
+ // Cardinal directions in ITRF frame
424
+ // South vector (1, 0, 0) in SEZ
425
+ const southSEZ = new Vector3D(1, 0, 0);
426
+ const south = this.transformVector(southSEZ, sez2ecef).normalized();
427
+ const north = south.scale(-1);
428
+
429
+ // Zenith vector (0, 0, 1) in SEZ
430
+ const zenithSEZ = new Vector3D(0, 0, 1);
431
+ const zenith = this.transformVector(zenithSEZ, sez2ecef).normalized();
432
+ const nadir = zenith.scale(-1);
433
+
434
+ const northJ2000 = this.transformVector(north, itrf2j2000).normalized();
435
+ const nadirJ2000 = this.transformVector(nadir, itrf2j2000).normalized();
436
+
437
+ // Position of site in J2000 scaled by the orbit altitude
438
+ const siteJ2000 = this.getPositionJ2000(orbitInsertionTime, positionITRF);
439
+ const scaleFactor = (orbitAltitudeKm + earthRadius) / earthRadius;
440
+ const r = siteJ2000.scale(scaleFactor);
441
+
442
+ // North vector rotated by azimuth along nadir vector scaled by the velocity of a circular orbit
443
+ const rLength = Math.sqrt(r.x * r.x + r.y * r.y + r.z * r.z);
444
+ const velocityMagnitude = Math.sqrt(mu / rLength);
445
+ const velocityDirection = this.rotateVector(northJ2000, nadirJ2000, azimuthRad);
446
+ const v = velocityDirection.scale(velocityMagnitude);
447
+
448
+ return {
449
+ position: [r.x, r.y, r.z],
450
+ velocity: [v.x, v.y, v.z],
451
+ epoch: orbitInsertionTime,
452
+ frame: ReferenceFrame.J2000,
453
+ };
454
+ }
455
+
456
+ static hohmannTransferWithIncZeroing(state0, targetSMA,
457
+ burnAtNodes = 1, mu = constants.MU / 1e9) {
458
+ let t0 = state0.epochUtc;
459
+ let i = null;
460
+
461
+ const prop = new BallisticPropagator(state0);
462
+
463
+ for (i = 1; i <= burnAtNodes; i++) {
464
+ const temp = prop.propagate(new Date(t0.getTime() + 1 * 1000));
465
+ t0 = this.#getNextNodeCrossingGeoOrbit(temp);
466
+ // here
467
+ }
468
+
469
+ const stateAtM1 = prop.propagate(t0);
470
+
471
+ // Initialize coords
472
+ const r0 = [stateAtM1.position.x, stateAtM1.position.y, stateAtM1.position.z];
473
+ const v0 = [stateAtM1.velocity.x, stateAtM1.velocity.y, stateAtM1.velocity.z];
474
+ const r0mag = norm(r0);
475
+ const v0mag = norm(v0);
476
+ const r1 = r0.map((component) => component * -1.0 * targetSMA / r0mag);
477
+ const r1mag = norm(r1);
478
+ const v1mag = Math.sqrt(mu / targetSMA);
479
+ const v1 = v0.map((component) => component * -1.0 * v1mag / v0mag);
480
+
481
+ // v0 and v1 unit vectors
482
+ const v0unit = v0.map((component) => component / v0mag);
483
+ const v1unit = v1.map((component) => component / v1mag);
484
+
485
+ // Transfer orbit
486
+ const smaTrans = (r0mag + r1mag) / 2;
487
+ const v0transMag = Math.sqrt(mu * (2 / r0mag - 1 / smaTrans));
488
+ const v1transMag = Math.sqrt(mu * (2 / r1mag - 1 / smaTrans));
489
+ const dv0 = v0unit.map((component) => component * (v0transMag - v0mag));
490
+ const dv1 = v1unit.map((component) => component * (v1mag - v1transMag));
491
+
492
+ // Transfer time
493
+ const t1 = new Date(t0.getTime()
494
+ + (Math.PI * Math.sqrt(Math.pow(smaTrans, 3) / mu)) * 1000);
495
+ const burn1 = {
496
+ time: t0,
497
+ dv: {x: 0, y: norm(dv0), z: 0}, // RTN/RIC frame!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
498
+ };
499
+
500
+ const rtnToEci = FrameConverter.rtnToEci(stateAtM1.position, stateAtM1.velocity);
501
+ const dv1eci = this.transformVector(burn1.dv, rtnToEci);
502
+
503
+ const state1 = {
504
+ position: new NodeVector3D(
505
+ stateAtM1.position.x,
506
+ stateAtM1.position.y,
507
+ stateAtM1.position.z,
508
+ ),
509
+ velocity: new NodeVector3D(
510
+ stateAtM1.velocity.x + dv1eci.x,
511
+ stateAtM1.velocity.y + dv1eci.y,
512
+ stateAtM1.velocity.z + dv1eci.z,
513
+ ),
514
+ epochUtc: t0,
515
+ };
516
+
517
+ const prop2 = new BallisticPropagator(state1);
518
+ const stateAtM2 = prop2.propagate(t1);
519
+
520
+ const rElementConv = [state0.position.x, state0.position.y, state0.position.z];
521
+ const vElementConv = [state0.velocity.x, state0.velocity.y, state0.velocity.z];
522
+ const elements0 = OrbitUtils.stateVectorToElements(rElementConv, vElementConv);
523
+ const incDiff = elements0.inclination;
524
+
525
+ const h = cross([state0.position.x, state0.position.y, state0.position.z],
526
+ [state0.velocity.x, state0.velocity.y, state0.velocity.z]);
527
+ const hUnit = h.map((component) => component / norm(h));
528
+ const node = cross([0, 0, 1], h);
529
+ let argLat = null;
530
+
531
+ function getAngleAligned(v1, v2, vN, rightHand) {
532
+ if (rightHand) {
533
+ return Math.atan2(dot(cross(v1, v2), vN), dot(v1, v2));
534
+ } else {
535
+ return Math.atan2(dot(cross(v2, v1), vN), dot(v1, v2));
536
+ }
537
+ }
538
+
539
+ const rightHand = true;
540
+ const initialPos = [state0.position.x, state0.position.y, state0.position.z];
541
+ argLat = getAngleAligned(node, initialPos, hUnit, rightHand);
542
+
543
+ if (burnAtNodes % 2 === 0) {
544
+ if (argLat >= 0 && argLat <= 180) {
545
+ argLat -= 180.0;
546
+ } else {
547
+ argLat += 180.0;
548
+ }
549
+ }
550
+
551
+ let dvC = -1 * v1mag * Math.sin(incDiff);
552
+ const dvI = -1 * v1mag * (1 - Math.cos(incDiff));
553
+ if (!(argLat >= 0 && argLat <= 180)) {
554
+ dvC *= -1;
555
+ }
556
+
557
+ const burn2 = {
558
+ time: t1,
559
+ dv: {x: 0, y: norm(dv1) + dvI, z: dvC},
560
+ };
561
+
562
+ const rtnToEci2 = FrameConverter.rtnToEci(stateAtM2.position, stateAtM2.velocity);
563
+ const dv1eci2 = this.transformVector(burn2.dv, rtnToEci2);
564
+
565
+ const state2 = {
566
+ position: new NodeVector3D(
567
+ stateAtM2.position.x,
568
+ stateAtM2.position.y,
569
+ stateAtM2.position.z,
570
+ ),
571
+ velocity: new NodeVector3D(
572
+ stateAtM2.velocity.x + dv1eci2.x,
573
+ stateAtM2.velocity.y + dv1eci2.y,
574
+ stateAtM2.velocity.z + dv1eci2.z,
575
+ ),
576
+ epochUtc: t1,
577
+ };
578
+
579
+ return {
580
+ state0,
581
+ burn1,
582
+ state1,
583
+ burn2,
584
+ state2,
585
+ };
586
+ }
587
+
588
+ static #getNextNodeCrossingGeoOrbit(sv) {
589
+ const geoElements = {
590
+ semiMajorAxis: 35786.0 + constants.EARTH_RADIUS_KM,
591
+ eccentricity: 0,
592
+ inclination: 0,
593
+ raan: 0,
594
+ argOfPeriapsis: 0,
595
+ trueAnomaly: 0,
596
+ };
597
+ const geoOrbitState = {
598
+ state: OrbitUtils.elementsToStateVector(geoElements),
599
+ epochUtc: sv.epochUtc,
600
+ };
601
+ const nextNode = this.#findMutualNodeTimes(sv, geoOrbitState);
602
+ return nextNode.nextNodeTime;
603
+ }
604
+
605
+ static #findMutualNodeTimes(sv1, sv2, muEarth = constants.MU / 1e9) {
606
+ // Implementation for finding mutual node times between two state vectors
607
+
608
+ if (!(sv1.epochUtc === sv2.epochUtc)) {
609
+ return "lol what on earth";
610
+ }
611
+
612
+ const orbit1 = OrbitUtils.stateVectorToElements(
613
+ [sv1.position.x, sv1.position.y, sv1.position.z],
614
+ [sv1.velocity.x, sv1.velocity.y, sv1.velocity.z]);
615
+ const orbit2 = OrbitUtils.stateVectorToElements(
616
+ sv2.state.position,
617
+ sv2.state.velocity);
618
+
619
+ const state1 = OrbitUtils.elementsToStateVector(orbit1);
620
+ const state2 = OrbitUtils.elementsToStateVector(orbit2);
621
+
622
+ const h1 = cross(state1.position, state1.velocity);
623
+ const h2 = cross(state2.position, state2.velocity);
624
+ const h1Norm = norm(h1);
625
+ const h2Norm = norm(h2);
626
+
627
+ const h1unit = h1.map((component) => component / h1Norm);
628
+ const h2unit = h2.map((component) => component / h2Norm);
629
+
630
+ function getAngleBetween2Vectors(vec1, vec2) {
631
+ const dotProduct = dot(vec1, vec2);
632
+ const normsProduct = norm(vec1) * norm(vec2);
633
+ return Math.acos(dotProduct / normsProduct);
634
+ }
635
+
636
+ const ascendingFirst = getAngleBetween2Vectors(state1.position, h2unit) > Math.PI / 2;
637
+
638
+ const nodalLine = cross(h1unit, h2unit);
639
+ const nodalLineUnit = nodalLine.map((component) => component / norm(nodalLine));
640
+ const nodalLineUnitVector3D = new Vector3D(
641
+ nodalLineUnit[0],
642
+ nodalLineUnit[1],
643
+ nodalLineUnit[2],
644
+ );
645
+
646
+ const rTilde1 = FrameConverter.perifocalToInertial(orbit1);
647
+ const rTilde1inverse = FrameConverter.invertMatrix(rTilde1);
648
+
649
+ const mutualNodeLinePerifocal = rTilde1inverse.multiplyVector3D(nodalLineUnitVector3D);
650
+ const slopeMutualNodalLinePerifocal = mutualNodeLinePerifocal.y / mutualNodeLinePerifocal.x;
651
+ const mutualNode = this.#findIntersectionEllipseLine( // inputs here are causing an error increase
652
+ slopeMutualNodalLinePerifocal,
653
+ orbit1.semiMajorAxis,
654
+ orbit1.eccentricity);
655
+
656
+ const q1 = mutualNode.vec1;
657
+ const q2 = mutualNode.vec2;
658
+ const q1unit = q1.map((component) => component / norm(q1));
659
+ const q2unit = q2.map((component) => component / norm(q2));
660
+
661
+ function mod(n, m) {
662
+ return ((n % m) + m) % m;
663
+ }
664
+
665
+ const n0 = mod(orbit1.trueAnomaly, 2 * Math.PI);
666
+ const n1 = mod(Math.atan2(dot(q1unit, [0, 1, 0]), dot(q1unit, [1, 0, 0])), 2 * Math.PI);
667
+ const n2 = mod(Math.atan2(dot(q2unit, [0, 1, 0]), dot(q2unit, [1, 0, 0])), 2 * Math.PI);
668
+
669
+ const e0 = OrbitUtils.trueAnomalyToEccentricAnomaly(n0, orbit1.eccentricity);
670
+ let e1 = OrbitUtils.trueAnomalyToEccentricAnomaly(n1, orbit1.eccentricity); // only matching to 2 sig figs w astrolib
671
+ let e2 = OrbitUtils.trueAnomalyToEccentricAnomaly(n2, orbit1.eccentricity); // only matching to 3 sig figs w astrolib
672
+
673
+ if (e1 < 0) {
674
+ e1 += 2 * Math.PI;
675
+ }
676
+
677
+ if (e2 < 0) {
678
+ e2 += 2 * Math.PI;
679
+ }
680
+
681
+ const k1 = n1 < n0 ? 1 : 0;
682
+ const k2 = n2 < n0 ? 1 : 0;
683
+
684
+ const dt1 = Math.sqrt(Math.pow(orbit1.semiMajorAxis, 3) / muEarth)
685
+ * (2 * k1 * Math.PI + (e1 - orbit1.eccentricity * Math.sin(e1))
686
+ - (e0 - orbit1.eccentricity * Math.sin(e0)));
687
+ const dt2 = Math.sqrt(
688
+ Math.pow(orbit1.semiMajorAxis, 3) / muEarth)
689
+ * (2 * k2 * Math.PI + (e2 - orbit1.eccentricity * Math.sin(e2))
690
+ - (e0 - orbit1.eccentricity * Math.sin(e0)));
691
+ let first = null;
692
+ let second = null;
693
+
694
+ if (dt2 > dt1) {
695
+ first = new Date(sv1.epochUtc.getTime() + dt1 * 1000);
696
+ second = new Date(sv2.epochUtc.getTime() + dt2 * 1000);
697
+ } else {
698
+ first = new Date(sv2.epochUtc.getTime() + dt2 * 1000);
699
+ second = new Date(sv1.epochUtc.getTime() + dt1 * 1000);
700
+ }
701
+
702
+ const next = first < second ? first : second;
703
+ const previous = first < second ? second : first;
704
+
705
+ if (ascendingFirst) {
706
+ return {
707
+ ascendingNodeTime: first,
708
+ descendingNodeTime: second,
709
+ nextNodeTime: next,
710
+ previousNodeTime: previous,
711
+ };
712
+ } else {
713
+ return {
714
+ ascendingNodeTime: second,
715
+ descendingNodeTime: first,
716
+ nextNodeTime: next,
717
+ previousNodeTime: previous,
718
+ };
719
+ }
720
+ }
721
+
722
+ static #findIntersectionEllipseLine(m, a, e) {
723
+ // Squares of inputs
724
+ const m2 = Math.pow(m, 2);
725
+ const a2 = Math.pow(a, 2);
726
+ const e2 = Math.pow(e, 2);
727
+
728
+ // The b constant, as found in the classic ellipse formulation.
729
+ const b = a * Math.sqrt(1 - Math.pow(e, 2));
730
+ const b2 = Math.pow(b, 2);
731
+
732
+ // Solving the quadratic equation (αx2 + βx + γ = 0)
733
+ const alpha = (a2 * m2) + b2;
734
+ const beta = 2 * a * b2 * e;
735
+ const gamma = (a2 * b2 * e2) - (a2 * b2);
736
+
737
+ const discriminant = Math.sqrt(Math.pow(beta, 2) - 4 * alpha * gamma);
738
+
739
+ if (discriminant < 0) {
740
+ return (new Vector3D(0, 0, 0), new Vector3D(0, 0, 0));
741
+ }
742
+
743
+ const x1 = (-beta + discriminant) / (2 * alpha);
744
+ const x2 = (-beta - discriminant) / (2 * alpha);
745
+ const y1 = m * x1;
746
+ const y2 = m * x2;
747
+
748
+ return {
749
+ vec1: [x1, y1, 0],
750
+ vec2: [x2, y2, 0],
751
+ };
752
+ }
753
+ }
754
+
755
+ class LaunchNominalOutput {
756
+ constructor(launchNominalType) {
757
+ this.launchNominalType = launchNominalType;
758
+ this.states = [];
759
+ this.burns = [];
760
+ }
761
+
762
+ addState(stateVector, elements, epoch) {
763
+ const pos = stateVector.position;
764
+ const vel = stateVector.velocity;
765
+
766
+ if (!elements) {
767
+ const j2000ToTeme = FrameConverter.getTransform("J2000", "TEME", epoch);
768
+ const posTeme = LaunchNominalClass.transformVector(pos, j2000ToTeme);
769
+ const velTeme = LaunchNominalClass.transformVector(vel, j2000ToTeme);
770
+ elements = OrbitUtils.stateVectorToElements(
771
+ [posTeme.x, posTeme.y, posTeme.z],
772
+ [velTeme.x, velTeme.y, velTeme.z]);
773
+ }
774
+
775
+ this.states.push({
776
+ epoch: epoch,
777
+ sv: {
778
+ r: [pos.x, pos.y, pos.z],
779
+ v: [vel.x, vel.y, vel.z],
780
+ },
781
+ elements: {
782
+ sma: elements.semiMajorAxis,
783
+ ecc: elements.eccentricity,
784
+ inc: elements.inclination,
785
+ raan: elements.raan,
786
+ argp: elements.argOfPeriapsis,
787
+ ta: elements.trueAnomaly,
788
+ },
789
+ // Placeholder for tle
790
+ });
791
+
792
+ return this;
793
+ }
794
+
795
+ addBurn(epoch, dv) {
796
+ this.burns.push({
797
+ epoch: epoch,
798
+ dv: [dv.x, dv.y, dv.z],
799
+ });
800
+
801
+ return this;
802
+ }
803
+
804
+ toJSON() {
805
+ if (this.burns.length === 0) {
806
+ return {
807
+ launchNominalType: this.launchNominalType,
808
+ states: this.states,
809
+ };
810
+ } else {
811
+ return {
812
+ launchNominalType: this.launchNominalType,
813
+ states: this.states,
814
+ burns: this.burns,
815
+ };
816
+ }
817
+ }
818
+ }
819
+
820
+ module.exports = {
821
+ LaunchNominalClass: LaunchNominalClass,
822
+ };