@saber-usa/node-common 1.7.2 → 1.7.3

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