From 8120b3f90bd8679a86281f36c05cfc240499e8a9 Mon Sep 17 00:00:00 2001 From: Theodore Kruczek Date: Sun, 14 Jan 2024 10:44:22 -0500 Subject: [PATCH] test: :white_check_mark: add tests for ClassicalElements --- examples/satellite-js-migration.ts | 4 + src/coordinate/ClassicalElements.ts | 176 +++++++++++------- src/coordinate/EquinoctialElements.ts | 11 +- src/coordinate/Tle.ts | 2 +- src/objects/Satellite.ts | 9 + test/coordinate/ClassicalElements.test.ts | 125 +++++++++++++ .../ClassicalElements.test.ts.snap | 91 +++++++++ 7 files changed, 349 insertions(+), 69 deletions(-) create mode 100644 test/coordinate/ClassicalElements.test.ts create mode 100644 test/coordinate/__snapshots__/ClassicalElements.test.ts.snap diff --git a/examples/satellite-js-migration.ts b/examples/satellite-js-migration.ts index 51953a2..41313fb 100644 --- a/examples/satellite-js-migration.ts +++ b/examples/satellite-js-migration.ts @@ -28,6 +28,10 @@ const satellite = new Satellite({ tle2, }); +const elements = satellite.getClassicalElements(new Date(1705109326817)); + +console.warn(elements); + // You can still propagate a satellite using time since epoch (in minutes), but it's not recommended. const timeSinceTleEpochMinutes = 10; const positionAndVelocity = Sgp4.propagate(satellite.satrec, timeSinceTleEpochMinutes); diff --git a/src/coordinate/ClassicalElements.ts b/src/coordinate/ClassicalElements.ts index f2cc084..2ae25a8 100644 --- a/src/coordinate/ClassicalElements.ts +++ b/src/coordinate/ClassicalElements.ts @@ -1,3 +1,4 @@ +import { Degrees, Kilometers, Radians, Seconds } from 'src/main'; import { Vector3D } from '../operations/Vector3D'; import { EpochUTC } from '../time/EpochUTC'; import { earthGravityParam, RAD2DEG, sec2min, secondsPerDay, TAU } from '../utils/constants'; @@ -8,35 +9,42 @@ import { PositionVelocity, StateVector } from './StateVector'; interface ClassicalElementsParams { epoch: EpochUTC; - semimajorAxis: number; + semimajorAxis: Kilometers; eccentricity: number; - inclination: number; - rightAscension: number; - argPerigee: number; - trueAnomaly: number; + inclination: Radians; + rightAscension: Radians; + argPerigee: Radians; + trueAnomaly: Radians; mu?: number; } - -// / Classical orbital elements. +/** + * 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 { - // / UTC epoch epoch: EpochUTC; - // / Semimajor-axis _(km)_. - semimajorAxis: number; - // / Eccentricity _(unitless)_. + semimajorAxis: Kilometers; eccentricity: number; - // / Inclination _(rad)_. - inclination: number; - // / Right-ascension of the ascending node _(rad)_. - rightAscension: number; - // / Argument of perigee _(rad)_. - argPerigee: number; - // / True anomaly _(rad)_. - trueAnomaly: number; - // / Gravitational parameter _(km³/s²)_. + inclination: Radians; + rightAscension: Radians; + argPerigee: Radians; + trueAnomaly: Radians; + /** Gravitational parameter in km³/s². */ mu: number; - /** Create a new [ClassicalElements] object from orbital elements. */ constructor({ epoch, semimajorAxis, @@ -62,11 +70,11 @@ export class ClassicalElements { * @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 classical elements are undefined for fixed frames. + * @throws Error if the StateVector is not in an inertial frame. */ static fromStateVector(state: StateVector, mu = earthGravityParam): ClassicalElements { if (!state.inertial) { - throw new Error('Classical elements are undefined for fixed frames.'); + throw new Error('State vector must be in inertial frame (like J2000).'); } const pos = state.position; const vel = state.velocity; @@ -96,44 +104,63 @@ export class ClassicalElements { return new ClassicalElements({ epoch: state.epoch, - semimajorAxis: a, + semimajorAxis: a as Kilometers, eccentricity: e, - inclination: i, - rightAscension: o, - argPerigee: w, - trueAnomaly: v, + inclination: i as Radians, + rightAscension: o as Radians, + argPerigee: w as Radians, + trueAnomaly: v as Radians, mu, }); } - /** Inclination _(°)_. */ - get inclinationDegrees(): number { - return this.inclination * RAD2DEG; + /** + * Gets the inclination in degrees. + * @returns The inclination in degrees. + */ + get inclinationDegrees(): Degrees { + return (this.inclination * RAD2DEG) as Degrees; } - /** Right-ascension of the ascending node _(°)_. */ - get rightAscensionDegrees(): number { - return this.rightAscension * RAD2DEG; + /** + * Gets the right ascension in degrees. + * @returns The right ascension in degrees. + */ + get rightAscensionDegrees(): Degrees { + return (this.rightAscension * RAD2DEG) as Degrees; } - /** Argument of perigee _(°)_. */ - get argPerigeeDegrees(): number { - return this.argPerigee * RAD2DEG; + /** + * Gets the argument of perigee in degrees. + * @returns The argument of perigee in degrees. + */ + get argPerigeeDegrees(): Degrees { + return (this.argPerigee * RAD2DEG) as Degrees; } - /** True anomaly _(°)_. */ - get trueAnomalyDegrees(): number { - return this.trueAnomaly * RAD2DEG; + /** + * Gets the true anomaly in degrees. + * @returns The true anomaly in degrees. + */ + get trueAnomalyDegrees(): Degrees { + return (this.trueAnomaly * RAD2DEG) as Degrees; } - /** Apogee distance from central body _(km)_. */ - get apogee(): number { - return this.semimajorAxis * (1.0 + this.eccentricity); + /** + * Gets the apogee of the classical elements. + * @returns The apogee in kilometers. + */ + get apogee(): Kilometers { + return (this.semimajorAxis * (1.0 + this.eccentricity)) as Kilometers; } - /** Perigee distance from central body _(km)_. */ + /** + * Gets the perigee of the classical elements. + * The perigee is the point in an orbit that is closest to the focus (in this case, the Earth). + * @returns The perigee distance in kilometers. + */ get perigee(): number { - return this.semimajorAxis * (1.0 - this.eccentricity); + return (this.semimajorAxis * (1.0 - this.eccentricity)) as Kilometers; } toString(): string { @@ -149,25 +176,37 @@ export class ClassicalElements { ].join('\n'); } - /** Compute the mean motion _(rad/s)_ of this orbit. */ - meanMotion(): number { - return Math.sqrt(this.mu / (this.semimajorAxis * this.semimajorAxis * this.semimajorAxis)); + /** + * Calculates the mean motion of the celestial object. + * @returns The mean motion in radians. + */ + get meanMotion(): Radians { + return Math.sqrt(this.mu / (this.semimajorAxis * this.semimajorAxis * this.semimajorAxis)) as Radians; } - /** Compute the period _(seconds)_ of this orbit. */ - period(): number { - return TAU * Math.sqrt(this.semimajorAxis ** 3 / this.mu); + /** + * Calculates the period of the orbit. + * @returns The period in seconds. + */ + get period(): Seconds { + return (TAU * Math.sqrt(this.semimajorAxis ** 3 / this.mu)) as Seconds; } - /** Compute the number of revolutions completed per day for this orbit. */ - revsPerDay(): number { - return secondsPerDay / this.period(); + /** + * Compute the number of revolutions completed per day for this orbit. + * @returns The number of revolutions per day. + */ + get revsPerDay(): number { + return secondsPerDay / this.period; } - // / Return the orbit regime for this orbit. + /** + * Returns the orbit regime based on the classical elements. + * @returns The orbit regime. + */ getOrbitRegime(): OrbitRegime { - const n = this.revsPerDay(); - const p = this.period() * sec2min; + const n = this.revsPerDay; + const p = this.period * sec2min; if (n >= 0.99 && n <= 1.01 && this.eccentricity < 0.01) { return OrbitRegime.GEO; @@ -185,7 +224,10 @@ export class ClassicalElements { return OrbitRegime.OTHER; } - // Convert this to inertial position and velocity vectors. + /** + * 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( @@ -199,23 +241,31 @@ export class ClassicalElements { return { position, velocity }; } - // Convert this to EquinoctialElements. + /** + * Converts the classical elements to equinoctial elements. + * @returns {EquinoctialElements} The equinoctial elements. + */ toEquinoctialElements(): EquinoctialElements { const fr = Math.abs(this.inclination - Math.PI) < 1e-9 ? -1 : 1; const af = this.eccentricity * Math.cos(this.argPerigee + fr * this.rightAscension); const ag = this.eccentricity * Math.sin(this.argPerigee + fr * this.rightAscension); const l = this.argPerigee + fr * this.rightAscension + newtonNu(this.eccentricity, this.trueAnomaly).m; - const n = this.meanMotion(); + const n = this.meanMotion; const chi = Math.tan(0.5 * this.inclination) ** fr * Math.sin(this.rightAscension); const psi = Math.tan(0.5 * this.inclination) ** fr * Math.cos(this.rightAscension); return new EquinoctialElements(this.epoch, af, ag, l, n, chi, psi, { mu: this.mu, fr }); } - // / Return elements propagated to the provided [propEpoch]. + /** + * 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 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)); @@ -247,7 +297,7 @@ export class ClassicalElements { inclination: this.inclination, rightAscension: this.rightAscension, argPerigee: this.argPerigee, - trueAnomaly: vFinal, + trueAnomaly: vFinal as Radians, mu: this.mu, }); } diff --git a/src/coordinate/EquinoctialElements.ts b/src/coordinate/EquinoctialElements.ts index 6912b09..b0d745a 100644 --- a/src/coordinate/EquinoctialElements.ts +++ b/src/coordinate/EquinoctialElements.ts @@ -1,3 +1,4 @@ +import { Kilometers, Radians } from 'src/main'; import { EpochUTC } from '../time/EpochUTC'; import { earthGravityParam, secondsPerDay, TAU } from '../utils/constants'; import { newtonM } from '../utils/functions'; @@ -70,12 +71,12 @@ export class EquinoctialElements { return new ClassicalElements({ epoch: this.epoch, - semimajorAxis: a, + semimajorAxis: a as Kilometers, eccentricity: e, - inclination: i, - rightAscension: o, - argPerigee: w, - trueAnomaly: v, + inclination: i as Radians, + rightAscension: o as Radians, + argPerigee: w as Radians, + trueAnomaly: v as Radians, mu: this.mu, }); } diff --git a/src/coordinate/Tle.ts b/src/coordinate/Tle.ts index 95ce772..654135d 100644 --- a/src/coordinate/Tle.ts +++ b/src/coordinate/Tle.ts @@ -251,7 +251,7 @@ export class Tle { const tles = FormatTle.createTle({ inc: FormatTle.inclination(elements.inclinationDegrees), - meanmo: FormatTle.meanMotion(elements.revsPerDay()), + meanmo: FormatTle.meanMotion(elements.revsPerDay), ecen: FormatTle.eccentricity(elements.eccentricity.toFixed(7)), argPe: FormatTle.argumentOfPerigee(elements.argPerigeeDegrees), meana: FormatTle.meanAnomaly(newtonNu(elements.eccentricity, elements.trueAnomaly).m * RAD2DEG), diff --git a/src/objects/Satellite.ts b/src/objects/Satellite.ts index 50fede1..5bddd8e 100644 --- a/src/objects/Satellite.ts +++ b/src/objects/Satellite.ts @@ -28,6 +28,7 @@ * SOFTWARE. */ +import type { ClassicalElements } from 'src/coordinate'; import { Geodetic } from '../coordinate/Geodetic'; import { ITRF } from '../coordinate/ITRF'; import { J2000 } from '../coordinate/J2000'; @@ -298,6 +299,14 @@ export class Satellite extends BaseObject { return RIC.fromJ2000(this.getJ2000(date), reference.getJ2000(date)); } + getTle(): Tle { + return new Tle(this.tle1, this.tle2); + } + + getClassicalElements(date: Date = new Date()): ClassicalElements { + return this.getJ2000(date).toClassicalElements(); + } + /** * Calculates the RAE (Range, Azimuth, Elevation) vector for a given sensor and time. * diff --git a/test/coordinate/ClassicalElements.test.ts b/test/coordinate/ClassicalElements.test.ts new file mode 100644 index 0000000..2f3603c --- /dev/null +++ b/test/coordinate/ClassicalElements.test.ts @@ -0,0 +1,125 @@ +import { ClassicalElements, EpochUTC, J2000, Kilometers, Radians, Vector3D } from './../../src/main'; + +describe('ClassicalElements', () => { + const epoch = EpochUTC.fromDateTime(new Date('2024-01-14T14:39:39.914Z')); + let elements: ClassicalElements; + + beforeEach(() => { + 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, + }); + }); + + // can be constructed with valid parameters + it('should construct a ClassicalElements object with valid parameters', () => { + expect(elements).toMatchSnapshot(); + }); + + // can convert to EquinoctialElements + it('should convert ClassicalElements to EquinoctialElements', () => { + const equinoctialElements = elements.toEquinoctialElements(); + + expect(equinoctialElements).toMatchSnapshot(); + }); + + // can propagate to a new epoch + it('should propagate ClassicalElements to a new epoch', () => { + const propEpoch = EpochUTC.fromDateTime(new Date('2024-01-15T14:39:39.914Z')); + const propagatedElements = elements.propagate(propEpoch); + + expect(propagatedElements).toMatchSnapshot(); + }); + + // can calculate mean motion + it('should calculate the mean motion of ClassicalElements', () => { + const meanMotion = elements.meanMotion; + + expect(meanMotion).toMatchSnapshot(); + }); + + // can calculate period + it('should calculate the period of ClassicalElements', () => { + const period = elements.period; + + expect(period).toMatchSnapshot(); + }); + + // can calculate apogee + it('should calculate the apogee of ClassicalElements', () => { + const apogee = elements.apogee; + + expect(apogee).toMatchSnapshot(); + }); + + // can calculate perigee + it('should calculate the perigee of ClassicalElements', () => { + const perigee = elements.perigee; + + expect(perigee).toMatchSnapshot(); + }); + + // can calculate orbit regime + it('should calculate the orbit regime of ClassicalElements', () => { + const orbitRegime = elements.getOrbitRegime(); + + expect(orbitRegime).toMatchSnapshot(); + }); + + // can convert to PositionVelocity + it('should convert ClassicalElements to PositionVelocity', () => { + const positionVelocity = elements.toPositionVelocity(); + + expect(positionVelocity).toMatchSnapshot(); + }); + + // can convert toString + it('should convert ClassicalElements to string', () => { + const string = elements.toString(); + + expect(string).toMatchSnapshot(); + }); + + // can calculate inclination in degrees + it('should calculate inclination in degrees', () => { + const inclination = elements.inclinationDegrees; + + expect(inclination).toMatchSnapshot(); + }); + // can calculate right ascension in degrees + it('should calculate right ascension in degrees', () => { + const rightAscension = elements.rightAscensionDegrees; + + expect(rightAscension).toMatchSnapshot(); + }); + // can calculate argument of perigee in degrees + it('should calculate argument of perigee in degrees', () => { + const argPerigee = elements.argPerigeeDegrees; + + expect(argPerigee).toMatchSnapshot(); + }); + // can calculate true anomaly in degrees + it('should calculate true anomaly in degrees', () => { + const trueAnomaly = elements.trueAnomalyDegrees; + + expect(trueAnomaly).toMatchSnapshot(); + }); + + // can create from StateVector + it('should create ClassicalElements from StateVector', () => { + const stateVector = new J2000( + EpochUTC.fromDateTime(new Date(1705109326817)), + new Vector3D(1538.223335842895, 5102.261204021967, 4432.634965003577), + new Vector3D(-7.341518909379302, 0.6516718453998644, 1.7933882499861993), + ); + + const classicalElements = ClassicalElements.fromStateVector(stateVector); + + expect(classicalElements).toMatchSnapshot(); + }); +}); diff --git a/test/coordinate/__snapshots__/ClassicalElements.test.ts.snap b/test/coordinate/__snapshots__/ClassicalElements.test.ts.snap new file mode 100644 index 0000000..66aee38 --- /dev/null +++ b/test/coordinate/__snapshots__/ClassicalElements.test.ts.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClassicalElements should calculate argument of perigee in degrees 1`] = `145.4534258954243`; + +exports[`ClassicalElements should calculate inclination in degrees 1`] = `43.02390063917958`; + +exports[`ClassicalElements should calculate right ascension in degrees 1`] = `1.6180073591471598`; + +exports[`ClassicalElements should calculate the apogee of ClassicalElements 1`] = `6951.349601958632`; + +exports[`ClassicalElements should calculate the mean motion of ClassicalElements 1`] = `0.0010911808567933578`; + +exports[`ClassicalElements should calculate the orbit regime of ClassicalElements 1`] = `"Low Earth Orbit"`; + +exports[`ClassicalElements should calculate the perigee of ClassicalElements 1`] = `6935.746105487338`; + +exports[`ClassicalElements should calculate the period of ClassicalElements 1`] = `5758.152068066809`; + +exports[`ClassicalElements should calculate true anomaly in degrees 1`] = `33.984415021959016`; + +exports[`ClassicalElements should construct a ClassicalElements object with valid parameters 1`] = ` +ClassicalElements { + "argPerigee": 2.5386411901807353, + "eccentricity": 0.0011235968124658146, + "epoch": EpochUTC { + "posix": 1705243179.914, + }, + "inclination": 0.7509087232045765, + "mu": 398600.4415, + "rightAscension": 0.028239555738616327, + "semimajorAxis": 6943.547853722985, + "trueAnomaly": 0.5931399364974058, +} +`; + +exports[`ClassicalElements should convert ClassicalElements to EquinoctialElements 1`] = ` +EquinoctialElements { + "af": -0.0009430897960304828, + "ag": 0.0006107793657340771, + "chi": 0.01112918196689249, + "epoch": EpochUTC { + "posix": 1705243179.914, + }, + "fr": 1, + "l": 3.15876545174161, + "mu": 398600.4415, + "n": 0.0010911808567933578, + "psi": 0.3939942790541112, +} +`; + +exports[`ClassicalElements should convert ClassicalElements to PositionVelocity 1`] = ` +Object { + "position": Vector3D { + "x": -6935.3813042612455, + "y": -146.1261328371035, + "z": 46.43907991639276, + }, + "velocity": Vector3D { + "x": 0.07740377357485445, + "y": -5.543955093986134, + "z": -5.174123893746047, + }, +} +`; + +exports[`ClassicalElements should convert ClassicalElements to string 1`] = ` +"[ClassicalElements] + Epoch: 2024-01-14T14:39:39.914Z + Semimajor Axis (a): 6943.5479 km + Eccentricity (e): 0.0011236 + Inclination (i): 43.0239° + Right Ascension (Ω): 1.6180° + Argument of Perigee (ω): 145.4534° + True Anomaly (ν): 33.9844°" +`; + +exports[`ClassicalElements should create ClassicalElements from StateVector 1`] = ` +ClassicalElements { + "argPerigee": 1.7497725502749213, + "eccentricity": 0.0006908917666211547, + "epoch": EpochUTC { + "posix": 1705109326.817, + }, + "inclination": 0.7503068041317154, + "mu": 398600.4415, + "rightAscension": 0.17555503341584255, + "semimajorAxis": 6935.754028093152, + "trueAnomaly": 5.749771010517163, +} +`;