UNPKG

ootk-core

Version:

Orbital Object Toolkit. A modern typed replacement for satellite.js including SGP4 propagation, TLE parsing, Sun and Moon calculations, and more.

327 lines (293 loc) 11.3 kB
/** * @author Theodore Kruczek. * @license MIT * @copyright (c) 2022-2025 Theodore Kruczek Permission is * hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the * Software without restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import { Minutes, PositionVelocity, Degrees, Kilometers, Radians, Seconds, KilometersPerSecond, Earth } from '../main.js'; import { Vector3D } from '../operations/Vector3D.js'; import { EpochUTC } from '../time/EpochUTC.js'; import { earthGravityParam, MINUTES_PER_DAY, RAD2DEG, sec2min, TAU } from '../utils/constants.js'; import { clamp, matchHalfPlane, newtonNu } from '../utils/functions.js'; import { EquinoctialElements } from './EquinoctialElements.js'; import { OrbitRegime } from '../enums/OrbitRegime.js'; import { StateVector } from './StateVector.js'; import { ClassicalElementsParams } from '../interfaces/ClassicalElementsParams.js'; /** * The ClassicalElements class represents the classical orbital elements of an object. * @example * ```ts * const epoch = EpochUTC.fromDateTime(new Date('2024-01-14T14:39:39.914Z')); * const elements = new ClassicalElements({ * epoch, * semimajorAxis: 6943.547853722985 as Kilometers, * eccentricity: 0.0011235968124658146, * inclination: 0.7509087232045765 as Radians, * rightAscension: 0.028239555738616327 as Radians, * argPerigee: 2.5386411901807353 as Radians, * trueAnomaly: 0.5931399364974058 as Radians, * }); * ``` */ export class ClassicalElements { epoch: EpochUTC; semimajorAxis: Kilometers; eccentricity: number; inclination: Radians; rightAscension: Radians; argPerigee: Radians; trueAnomaly: Radians; /** Gravitational parameter in km³/s². */ mu: number; constructor({ epoch, semimajorAxis, eccentricity, inclination, rightAscension, argPerigee, trueAnomaly, mu = earthGravityParam, }: ClassicalElementsParams) { this.epoch = epoch; this.semimajorAxis = semimajorAxis; this.eccentricity = eccentricity; this.inclination = inclination; this.rightAscension = rightAscension; this.argPerigee = argPerigee; this.trueAnomaly = trueAnomaly; this.mu = mu; } /** * Creates a new instance of ClassicalElements from a StateVector. * @param state The StateVector to convert. * @param mu The gravitational parameter of the central body. Default value is Earth's gravitational parameter. * @returns A new instance of ClassicalElements. * @throws Error if the StateVector is not in an inertial frame. */ static fromStateVector(state: StateVector, mu = earthGravityParam): ClassicalElements { if (!state.inertial) { throw new Error('State vector must be in inertial frame (like J2000).'); } const pos = state.position; const vel = state.velocity; const a = state.semimajorAxis; const eVecA = pos.scale(vel.magnitude() ** 2 - mu / pos.magnitude() as KilometersPerSecond); const eVecB = vel.scale(pos.dot(vel)); const eVec = eVecA.subtract(eVecB).scale(1 / mu); const e = eVec.magnitude(); const h = pos.cross(vel); const i = Math.acos(clamp(h.z / h.magnitude(), -1.0, 1.0)) as Radians; const n = Vector3D.zAxis.cross(h); let o = Math.acos(clamp(n.x / n.magnitude(), -1.0, 1.0)) as Radians; if (n.y < 0) { o = TAU - o as Radians; } let w = n.angle(eVec); if (eVec.z < 0) { w = TAU - w as Radians; } let v = eVec.angle(pos); if (pos.dot(vel) < 0) { v = TAU - v as Radians; } return new ClassicalElements({ epoch: state.epoch, semimajorAxis: a, eccentricity: e, inclination: i, rightAscension: o, argPerigee: w, trueAnomaly: v, mu, }); } /** * Gets the inclination in degrees. * @returns The inclination in degrees. */ get inclinationDegrees(): Degrees { return (this.inclination * RAD2DEG) as Degrees; } /** * Gets the right ascension in degrees. * @returns The right ascension in degrees. */ get rightAscensionDegrees(): Degrees { return (this.rightAscension * RAD2DEG) as Degrees; } /** * Gets the argument of perigee in degrees. * @returns The argument of perigee in degrees. */ get argPerigeeDegrees(): Degrees { return (this.argPerigee * RAD2DEG) as Degrees; } /** * Gets the true anomaly in degrees. * @returns The true anomaly in degrees. */ get trueAnomalyDegrees(): Degrees { return (this.trueAnomaly * RAD2DEG) as Degrees; } /** * Gets the apogee of the classical elements. It is measured from the surface of the earth. * @returns The apogee in kilometers. */ get apogee(): Kilometers { return (this.semimajorAxis * (1.0 + this.eccentricity) - Earth.radiusMean) as Kilometers; } /** * Gets the perigee of the classical elements. The perigee is the point in an * orbit that is closest to the surface of the earth. * @returns The perigee distance in kilometers. */ get perigee(): number { return (this.semimajorAxis * (1.0 - this.eccentricity) - Earth.radiusMean) as Kilometers; } toString(): string { return [ '[ClassicalElements]', ` Epoch: ${this.epoch}`, ` Semimajor Axis (a): ${this.semimajorAxis.toFixed(4)} km`, ` Eccentricity (e): ${this.eccentricity.toFixed(7)}`, ` Inclination (i): ${this.inclinationDegrees.toFixed(4)}°`, ` Right Ascension (Ω): ${this.rightAscensionDegrees.toFixed(4)}°`, ` Argument of Perigee (ω): ${this.argPerigeeDegrees.toFixed(4)}°`, ` True Anomaly (ν): ${this.trueAnomalyDegrees.toFixed(4)}°`, ].join('\n'); } /** * Calculates the mean motion of the celestial object. * @returns The mean motion in radians. */ get meanMotion(): Radians { return Math.sqrt(this.mu / this.semimajorAxis ** 3) as Radians; } /** * Calculates the period of the orbit. * @returns The period in seconds. */ get period(): Minutes { const periodSec = (TAU * Math.sqrt(this.semimajorAxis ** 3 / this.mu)) as Seconds; return (periodSec / 60) as Minutes; } /** * Compute the number of revolutions completed per day for this orbit. * @returns The number of revolutions per day. */ get revsPerDay(): number { return MINUTES_PER_DAY / this.period; } /** * Returns the orbit regime based on the classical elements. * @returns The orbit regime. */ getOrbitRegime(): OrbitRegime { const n = this.revsPerDay; const p = this.period * sec2min; if (n >= 0.99 && n <= 1.01 && this.eccentricity < 0.01) { return OrbitRegime.GEO; } if (p >= 600 && p <= 800 && this.eccentricity <= 0.25) { return OrbitRegime.MEO; } if (n >= 11.25 && this.eccentricity <= 0.25) { return OrbitRegime.LEO; } if (this.eccentricity > 0.25) { return OrbitRegime.HEO; } return OrbitRegime.OTHER; } /** * Converts the classical orbital elements to position and velocity vectors. * @returns An object containing the position and velocity vectors. */ toPositionVelocity(): PositionVelocity { const rVec = new Vector3D(Math.cos(this.trueAnomaly), Math.sin(this.trueAnomaly), 0.0); const rPQW = rVec.scale( (this.semimajorAxis * (1.0 - this.eccentricity ** 2)) / (1.0 + this.eccentricity * Math.cos(this.trueAnomaly)), ); const vVec = new Vector3D(-Math.sin(this.trueAnomaly), this.eccentricity + Math.cos(this.trueAnomaly), 0.0); const vPQW = vVec.scale(Math.sqrt(this.mu / (this.semimajorAxis * (1 - this.eccentricity ** 2)))); const position = rPQW .rotZ(-this.argPerigee as Radians) .rotX(-this.inclination) .rotZ(-this.rightAscension as Radians) as Vector3D<Kilometers>; const velocity = vPQW .rotZ(-this.argPerigee as Radians) .rotX(-this.inclination) .rotZ(-this.rightAscension as Radians) as Vector3D<KilometersPerSecond>; return { position, velocity }; } /** * Converts the classical elements to equinoctial elements. * @returns The equinoctial elements. */ toEquinoctialElements(): EquinoctialElements { const I = this.inclination > Math.PI / 2 ? -1 : 1; const h = this.eccentricity * Math.sin(this.argPerigee + I * this.rightAscension); const k = this.eccentricity * Math.cos(this.argPerigee + I * this.rightAscension); const meanAnomaly = newtonNu(this.eccentricity, this.trueAnomaly).m; const lambda = (meanAnomaly + this.argPerigee + I * this.rightAscension) as Radians; const a = this.semimajorAxis; const p = Math.tan(0.5 * this.inclination) ** I * Math.sin(this.rightAscension); const q = Math.tan(0.5 * this.inclination) ** I * Math.cos(this.rightAscension); return new EquinoctialElements({ epoch: this.epoch, k, h, lambda, a, p, q, mu: this.mu, I }); } /** * Propagates the classical elements to a given epoch. * @param propEpoch - The epoch to propagate the classical elements to. * @returns The classical elements at the propagated epoch. */ propagate(propEpoch: EpochUTC): ClassicalElements { const t = this.epoch; const n = this.meanMotion; const delta = propEpoch.difference(t); const cosV = Math.cos(this.trueAnomaly); let eaInit = Math.acos(clamp((this.eccentricity + cosV) / (1 + this.eccentricity * cosV), -1, 1)); eaInit = matchHalfPlane(eaInit, this.trueAnomaly); let maInit = eaInit - this.eccentricity * Math.sin(eaInit); maInit = matchHalfPlane(maInit, eaInit); const maFinal = (maInit + n * delta) % TAU; let eaFinal = maFinal; for (let iter = 0; iter < 32; iter++) { const eaTemp = maFinal + this.eccentricity * Math.sin(eaFinal); if (Math.abs(eaTemp - eaFinal) < 1e-12) { break; } eaFinal = eaTemp; } const cosEaFinal = Math.cos(eaFinal); let vFinal = clamp(Math.acos((cosEaFinal - this.eccentricity) / (1 - this.eccentricity * cosEaFinal)), -1, 1); vFinal = matchHalfPlane(vFinal, eaFinal); return new ClassicalElements({ epoch: propEpoch, semimajorAxis: this.semimajorAxis, eccentricity: this.eccentricity, inclination: this.inclination, rightAscension: this.rightAscension, argPerigee: this.argPerigee, trueAnomaly: vFinal as Radians, mu: this.mu, }); } }