@saber-usa/node-common 1.7.7-alpha.1 → 1.7.7

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