├── .gitignore ├── .npmignore ├── src ├── coordinates │ ├── coordinate-interface.ts │ ├── look-angle.ts │ ├── geodetic.ts │ ├── teme.ts │ ├── classical-elements.ts │ ├── ric.ts │ ├── itrf.ts │ └── j2000.ts ├── forces │ ├── forces-interface.ts │ ├── solar-radiation-pressure.ts │ ├── atmospheric-drag.ts │ ├── third-body.ts │ ├── force-model.ts │ └── earth-gravity.ts ├── propagators │ ├── propagator-interface.ts │ ├── kepler-propagator.ts │ ├── interpolator-propagator.ts │ └── runge-kutta-4-propagator.ts ├── time │ ├── time-scales.ts │ ├── abstract-epoch.ts │ └── epoch-utc.ts ├── math │ ├── constants.ts │ ├── monte-carlo.ts │ ├── random-gaussian.ts │ ├── spherical-harmonics.ts │ ├── vector-6d.ts │ ├── operations.ts │ ├── matrix-3d.ts │ └── vector-3d.ts ├── data │ ├── values │ │ ├── finals.ts │ │ ├── exponential-atmosphere.ts │ │ ├── leap-seconds.ts │ │ └── iau1980.ts │ └── data-handler.ts ├── index.ts ├── conjunction │ └── conjunction-assessment.ts ├── bodies │ ├── moon-body.ts │ ├── sun-body.ts │ └── earth-body.ts ├── test │ ├── bodies-test.ts │ ├── propagator-test.ts │ └── coordinates-test.ts └── examples │ ├── conjunction-example.ts │ ├── basic-example.ts │ └── propagator-example.ts ├── tsconfig.json ├── LICENSE.TXT ├── package.json └── README.MD /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | bundle/ 3 | dist/ 4 | node_modules/ 5 | pious-squid-*.tgz 6 | scratchpad.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .vscode/ 4 | bundle/ 5 | dist/examples 6 | dist/test 7 | examples/ 8 | scratchpad.js 9 | src/ 10 | test/ 11 | tsconfig.json -------------------------------------------------------------------------------- /src/coordinates/coordinate-interface.ts: -------------------------------------------------------------------------------- 1 | import { Vector3D } from "../math/vector-3d"; 2 | import { EpochUTC } from "../time/epoch-utc"; 3 | 4 | export interface IStateVector { 5 | /** Satellite state epoch. */ 6 | epoch: EpochUTC; 7 | /** Position vector, in kilometers. */ 8 | position: Vector3D; 9 | /** Velocity vector, in kilometers per second. */ 10 | velocity: Vector3D; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/forces/forces-interface.ts: -------------------------------------------------------------------------------- 1 | import { J2000 } from "../coordinates/j2000"; 2 | import { Vector3D } from "../math/vector-3d"; 3 | 4 | /** Store acceration name and vector. */ 5 | export type AccelerationMap = { 6 | [name: string]: Vector3D; 7 | }; 8 | 9 | export interface AccelerationForce { 10 | /** Update acceleration map with a named vector for this object. */ 11 | acceleration: (j2kState: J2000, accMap: AccelerationMap) => void; 12 | } 13 | -------------------------------------------------------------------------------- /src/propagators/propagator-interface.ts: -------------------------------------------------------------------------------- 1 | import { J2000 } from "../coordinates/j2000"; 2 | import { EpochUTC } from "../time/epoch-utc"; 3 | 4 | /** Common interface for propagator objects. */ 5 | export interface IPropagator { 6 | /** Cache for last computed statellite state. */ 7 | state: J2000; 8 | /** Propagate state to a new epoch. */ 9 | propagate(epoch: EpochUTC): J2000; 10 | /** Propagate state by some number of seconds, repeatedly. */ 11 | step(epoch: EpochUTC, interval: number, count: number): J2000[]; 12 | /** Restore initial propagator state. */ 13 | reset(): void; 14 | } 15 | -------------------------------------------------------------------------------- /src/time/time-scales.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEpoch } from "./abstract-epoch"; 2 | 3 | /** UT1 Epoch */ 4 | export class EpochUT1 extends AbstractEpoch { 5 | constructor(millis: number) { 6 | super(millis); 7 | } 8 | } 9 | 10 | /** International Atomic Time (TAI) Epoch */ 11 | export class EpochTAI extends AbstractEpoch { 12 | constructor(millis: number) { 13 | super(millis); 14 | } 15 | } 16 | 17 | /** Terrestrial Time (TT) Epoch */ 18 | export class EpochTT extends AbstractEpoch { 19 | constructor(millis: number) { 20 | super(millis); 21 | } 22 | } 23 | 24 | /** Barycentric Dynamical Time (TDB) Epoch */ 25 | export class EpochTDB extends AbstractEpoch { 26 | constructor(millis: number) { 27 | super(millis); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/math/constants.ts: -------------------------------------------------------------------------------- 1 | /** Value of 2 times Pi. */ 2 | export const TWO_PI = Math.PI * 2; 3 | 4 | /** Unit for converting degrees to radians. */ 5 | export const DEG2RAD = Math.PI / 180; 6 | 7 | /** Unit for converting radians to degrees. */ 8 | export const RAD2DEG = 180 / Math.PI; 9 | 10 | /** Unit for converting 0.0001 arcseconds to radians. */ 11 | export const TTASEC2RAD = (1 / 60 / 60 / 10000) * DEG2RAD; 12 | 13 | /** Unit for converting arcseconds to radians. */ 14 | export const ASEC2RAD = (1 / 60 / 60) * DEG2RAD; 15 | 16 | /** Astronomical Unit, in kilometers. */ 17 | export const ASTRONOMICAL_UNIT = 149597870.0; 18 | 19 | /** Unit for converting seconds to days. */ 20 | export const SEC2DAY = 1 / 60 / 60 / 24; 21 | 22 | /** Unit for converting seconds to degrees. */ 23 | export const SEC2DEG = 1 / 60 / 60; 24 | 25 | /** Speed of light, in km/s. */ 26 | export const SPEED_OF_LIGHT = 299792458; 27 | -------------------------------------------------------------------------------- /src/data/values/finals.ts: -------------------------------------------------------------------------------- 1 | export interface FinalsData { 2 | /** USNO modified julaian date */ 3 | mjd: number; 4 | /** polar motion x-component (degrees) */ 5 | pmX: number; 6 | /** polar motion y-component (degrees) */ 7 | pmY: number; 8 | /** delta ut1 time (seconds) */ 9 | dut1: number; 10 | /** length of day (seconds) */ 11 | lod: number; 12 | /** delta psi (degrees) */ 13 | dPsi: number; 14 | /** delta epsilon (degrees) */ 15 | dEps: number; 16 | } 17 | 18 | /** IERS finals.all data. */ 19 | export let FINALS: FinalsData[] = []; 20 | 21 | /** Clear cached finals.all data. */ 22 | export function clearFinals() { 23 | FINALS = []; 24 | } 25 | 26 | /** Sort finals.all data by modified julian date. */ 27 | export function sortFinals() { 28 | FINALS.sort((a, b) => a.mjd - b.mjd); 29 | } 30 | 31 | /** Return a finals entry with all values set to zero. */ 32 | export function zeroFinal(): FinalsData { 33 | return { mjd: 0, pmX: 0, pmY: 0, dut1: 0, lod: 0, dPsi: 0, dEps: 0 }; 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // bodies 2 | export { EarthBody } from "./bodies/earth-body"; 3 | export { MoonBody } from "./bodies/moon-body"; 4 | export { SunBody } from "./bodies/sun-body"; 5 | // conjunction 6 | export { ConjunctionAssesment } from "./conjunction/conjunction-assessment"; 7 | // coordinates 8 | export { ClassicalElements } from "./coordinates/classical-elements"; 9 | export { Geodetic } from "./coordinates/geodetic"; 10 | export { ITRF } from "./coordinates/itrf"; 11 | export { J2000 } from "./coordinates/j2000"; 12 | // data 13 | export { DataHandler } from "./data/data-handler"; 14 | // math 15 | export { Matrix3D } from "./math/matrix-3d"; 16 | export { MonteCarlo } from "./math/monte-carlo"; 17 | export { Vector3D } from "./math/vector-3d"; 18 | // propagators 19 | export { InterpolatorPropagator } from "./propagators/interpolator-propagator"; 20 | export { KeplerPropagator } from "./propagators/kepler-propagator"; 21 | export { RungeKutta4Propagator } from "./propagators/runge-kutta-4-propagator"; 22 | // time 23 | export { EpochUTC } from "./time/epoch-utc"; 24 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 David RC Dayton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the “Software”), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/data/values/exponential-atmosphere.ts: -------------------------------------------------------------------------------- 1 | /** base altitude (km), nominal density (kg/m^3), scale height (km) */ 2 | export type ExponentialAtmosphereData = [number, number, number]; 3 | 4 | /** Exponential Atmospheric Model values. */ 5 | export const EXPONENTIAL_ATMOSPHERE: ExponentialAtmosphereData[] = [ 6 | [0, 1.225, 7.249], 7 | [25, 3.899e-2, 6.349], 8 | [30, 1.774e-2, 6.682], 9 | [40, 3.972e-3, 7.554], 10 | [50, 1.057e-3, 8.382], 11 | [60, 3.206e-4, 7.714], 12 | [70, 8.77e-5, 6.549], 13 | [80, 1.905e-5, 5.799], 14 | [90, 3.396e-6, 5.382], 15 | [100, 5.297e-7, 5.877], 16 | [110, 9.661e-8, 7.263], 17 | [120, 2.438e-8, 9.473], 18 | [130, 8.484e-9, 12.636], 19 | [140, 3.845e-9, 16.149], 20 | [150, 2.07e-9, 22.523], 21 | [180, 5.464e-10, 29.74], 22 | [200, 2.789e-10, 37.105], 23 | [250, 7.248e-11, 45.546], 24 | [300, 2.418e-11, 53.628], 25 | [350, 9.518e-12, 53.298], 26 | [400, 3.725e-12, 58.515], 27 | [450, 1.585e-12, 60.828], 28 | [500, 6.967e-13, 63.822], 29 | [600, 1.454e-13, 71.835], 30 | [700, 3.614e-14, 88.667], 31 | [800, 1.17e-14, 124.64], 32 | [900, 5.245e-15, 181.05], 33 | [1000, 3.019e-15, 268.0] 34 | ]; 35 | -------------------------------------------------------------------------------- /src/coordinates/look-angle.ts: -------------------------------------------------------------------------------- 1 | import { RAD2DEG } from "../math/constants"; 2 | 3 | /** Class representing look angles. */ 4 | export class LookAngle { 5 | /** Azimuth angle, in radians. */ 6 | public readonly azimuth: number; 7 | /** Elevation angle, in radians. */ 8 | public readonly elevation: number; 9 | /** Slant range, in kilometers. */ 10 | public readonly range: number; 11 | 12 | /** 13 | * Create a new LookAngle object. 14 | * 15 | * @param azimuth azimuth angle, in radians 16 | * @param elevation elevation angle, in radians 17 | * @param range slant range, in kilometers 18 | */ 19 | constructor(azimuth: number, elevation: number, range: number) { 20 | this.azimuth = azimuth; 21 | this.elevation = elevation; 22 | this.range = range; 23 | } 24 | 25 | /** Return a string representation of the object. */ 26 | public toString(): string { 27 | const { azimuth, elevation, range } = this; 28 | const output = [ 29 | "[Look-Angle]", 30 | ` Azimuth: ${(azimuth * RAD2DEG).toFixed(3)}\u00b0`, 31 | ` Elevation: ${(elevation * RAD2DEG).toFixed(3)}\u00b0`, 32 | ` Range: ${range.toFixed(3)} km` 33 | ]; 34 | return output.join("\n"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/math/monte-carlo.ts: -------------------------------------------------------------------------------- 1 | import { Matrix3D } from "./matrix-3d"; 2 | import { Vector3D } from "./vector-3d"; 3 | import { RandomGaussian } from "./random-gaussian"; 4 | 5 | /** Class for performing Monte-Carlo simulations. */ 6 | export class MonteCarlo { 7 | /** simulation vector */ 8 | private readonly vector: Vector3D; 9 | /** simulation covariance */ 10 | private readonly covariance: Matrix3D; 11 | /** random gaussian generator */ 12 | private readonly random: RandomGaussian; 13 | 14 | /** 15 | * Create a new MonteCarlo object. 16 | * 17 | * @param vector simulation vector 18 | * @param covariance simulation covariance 19 | * @param sigma standard deviation 20 | */ 21 | constructor(vector: Vector3D, covariance: Matrix3D, sigma: number) { 22 | this.vector = vector; 23 | this.covariance = covariance.scale(sigma).cholesky(); 24 | this.random = new RandomGaussian(0, 1); 25 | } 26 | 27 | /** 28 | * Sample the simulation space, and return a new statistically 29 | * relevent vector. 30 | */ 31 | public sample() { 32 | const gauss = this.random.nextVector3D(); 33 | const offset = this.covariance.multiplyVector3D(gauss); 34 | return this.vector.add(offset); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/conjunction/conjunction-assessment.ts: -------------------------------------------------------------------------------- 1 | import { Vector3D } from "../math/vector-3d"; 2 | import { Matrix3D } from "../math/matrix-3d"; 3 | import { MonteCarlo } from "../math/monte-carlo"; 4 | 5 | /** Class for satellite conjunction operations. */ 6 | export class ConjunctionAssesment { 7 | /** 8 | * Simulate possible outcomes of a satellite conjunction, using information 9 | * found in a Conjunction Summary Message (CSM). 10 | * 11 | * @param posA asset position 12 | * @param covA asset covariance 13 | * @param posB satellite position 14 | * @param covB satellite covariance 15 | * @param sigma standard deviation 16 | * @param iterations number of samples 17 | */ 18 | public static simulateConjunctionMessage( 19 | posA: Vector3D, 20 | covA: Matrix3D, 21 | posB: Vector3D, 22 | covB: Matrix3D, 23 | sigma: number, 24 | iterations: number 25 | ) { 26 | const mcA = new MonteCarlo(posA, covA, sigma); 27 | const mcB = new MonteCarlo(posB, covB, sigma); 28 | const missDistances: number[] = []; 29 | for (let i = 0; i < iterations; i++) { 30 | const sampleA = mcA.sample(); 31 | const sampleB = mcB.sample(); 32 | missDistances.push(sampleA.distance(sampleB)); 33 | } 34 | return missDistances; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/data/values/leap-seconds.ts: -------------------------------------------------------------------------------- 1 | /** julian date, offset (seconds) */ 2 | export type LeapSecondsData = [number, number]; 3 | 4 | /** List of leap seconds by Julian Date. */ 5 | export let LEAP_SECONDS: LeapSecondsData[] = [ 6 | [2437300.5, 1.422818], 7 | [2437512.5, 1.372818], 8 | [2437665.5, 1.845858], 9 | [2438334.5, 1.845858], 10 | [2438395.5, 3.24013], 11 | [2438486.5, 3.34013], 12 | [2438639.5, 3.44013], 13 | [2438761.5, 3.54013], 14 | [2438820.5, 3.64013], 15 | [2438942.5, 3.74013], 16 | [2439004.5, 3.84013], 17 | [2439126.5, 4.31317], 18 | [2439887.5, 4.21317], 19 | [2441317.5, 10.0], 20 | [2441499.5, 11.0], 21 | [2441683.5, 12.0], 22 | [2442048.5, 13.0], 23 | [2442413.5, 14.0], 24 | [2442778.5, 15.0], 25 | [2443144.5, 16.0], 26 | [2443509.5, 17.0], 27 | [2443874.5, 18.0], 28 | [2444239.5, 19.0], 29 | [2444786.5, 20.0], 30 | [2445151.5, 21.0], 31 | [2445516.5, 22.0], 32 | [2446247.5, 23.0], 33 | [2447161.5, 24.0], 34 | [2447892.5, 25.0], 35 | [2448257.5, 26.0], 36 | [2448804.5, 27.0], 37 | [2449169.5, 28.0], 38 | [2449534.5, 29.0], 39 | [2450083.5, 30.0], 40 | [2450630.5, 31.0], 41 | [2451179.5, 32.0], 42 | [2453736.5, 33.0], 43 | [2454832.5, 34.0], 44 | [2456109.5, 35.0], 45 | [2457204.5, 36.0], 46 | [2457754.5, 37.0] 47 | ]; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pious-squid", 3 | "version": "2.3.0", 4 | "description": "Orbital mechanics and satellite mission analysis library, for NodeJS and the browser.", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/david-rc-dayton/pious-squid.git" 10 | }, 11 | "scripts": { 12 | "build": "tsc", 13 | "bundle": "browserify dist/index.js -s PiousSquid -o bundle/pious-squid.js", 14 | "clean-build": "npm run clean && npm run build", 15 | "clean-bundle": "npm run clean-build && npm run bundle && npm run minify", 16 | "clean": "rimraf dist bundle pious-squid-*.tgz", 17 | "minify": "uglifyjs bundle/pious-squid.js > bundle/pious-squid.min.js", 18 | "prepack": "npm run clean-bundle", 19 | "test": "mocha ./dist/test/**/*.js", 20 | "watch": "tsc --watch" 21 | }, 22 | "keywords": [ 23 | "astrodynamics", 24 | "coordinates", 25 | "look-angles", 26 | "orbital-mechanics", 27 | "propagator", 28 | "satellite", 29 | "typescript" 30 | ], 31 | "author": "David RC Dayton", 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@types/mocha": "^5.2.5", 35 | "@types/node": "^10.12.18", 36 | "browserify": "^16.2.3", 37 | "mocha": "^5.2.0", 38 | "rimraf": "^2.6.3", 39 | "typescript": "^3.2.2", 40 | "uglify-js": "^3.4.9" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/time/abstract-epoch.ts: -------------------------------------------------------------------------------- 1 | /** Base class for representing epochs. */ 2 | export abstract class AbstractEpoch { 3 | /** Seconds since 1 January 1970, 00:00 UTC. */ 4 | public readonly unix: number; 5 | 6 | /** 7 | * Create a new Epoch object. 8 | * 9 | * @param millis milliseconds since 1 January 1970, 00:00 UTC 10 | */ 11 | constructor(millis: number) { 12 | this.unix = millis / 1000; 13 | } 14 | 15 | /** 16 | * Calculate the difference between this and another epoch, in seconds. 17 | * 18 | * @param epoch comparison epoch 19 | */ 20 | public difference(epoch: AbstractEpoch) { 21 | return this.unix - epoch.unix; 22 | } 23 | 24 | /** 25 | * Return true if this and another epoch are equal. 26 | * 27 | * @param epoch comparison epoch 28 | */ 29 | public equals(epoch: AbstractEpoch) { 30 | return this.unix == epoch.unix; 31 | } 32 | 33 | /** Convert this to a JavaScript Date object. */ 34 | public toDate() { 35 | return new Date(this.unix * 1000); 36 | } 37 | 38 | /** String representation of this object. */ 39 | public toString() { 40 | return this.toDate().toISOString(); 41 | } 42 | 43 | /** Convert this to a Julian Date. */ 44 | public toJulianDate() { 45 | return this.unix / 86400 + 2440587.5; 46 | } 47 | 48 | /** Convert this to Julian Centuries. */ 49 | public toJulianCenturies() { 50 | return (this.toJulianDate() - 2451545) / 36525; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/math/random-gaussian.ts: -------------------------------------------------------------------------------- 1 | import { TWO_PI } from "./constants"; 2 | import { Vector3D } from "./vector-3d"; 3 | 4 | /** 5 | * Class for generating random Gaussian numbers. 6 | * 7 | * Uses the Box-Mueller Transform for generating gaussian numbers from uniformly 8 | * distributed numbers. 9 | */ 10 | export class RandomGaussian { 11 | /** mean value */ 12 | private mu: number; 13 | /** standard deviation */ 14 | private sigma: number; 15 | /** gaussian storage 0 */ 16 | private z0: number; 17 | /** gaussian storage 1 */ 18 | private z1: number; 19 | /** uniform storage 1 */ 20 | private u1: number; 21 | /** uniform storage 2 */ 22 | private u2: number; 23 | /** should generate new values if true */ 24 | private generate: boolean; 25 | 26 | /** 27 | * Create a RandomGaussian object. 28 | * 29 | * @param mu mean value 30 | * @param sigma standard deviation 31 | */ 32 | constructor(mu: number, sigma: number) { 33 | this.mu = mu; 34 | this.sigma = sigma; 35 | this.z0 = 0; 36 | this.z1 = 0; 37 | this.u1 = 0; 38 | this.u2 = 0; 39 | this.generate = true; 40 | } 41 | 42 | /** Return the next random Gaussian number. */ 43 | public next() { 44 | if (!this.generate) { 45 | return this.z1 * this.sigma + this.mu; 46 | } 47 | 48 | do { 49 | this.u1 = Math.random(); 50 | this.u2 = Math.random(); 51 | } while (this.u1 <= Number.MIN_VALUE); 52 | 53 | const prefix = Math.sqrt(-2.0 * Math.log(this.u1)); 54 | this.z0 = prefix * Math.cos(TWO_PI * this.u2); 55 | this.z1 = prefix * Math.sin(TWO_PI * this.u2); 56 | return this.z0 * this.sigma + this.mu; 57 | } 58 | 59 | /** Return the next random Gaussian Vector3D object. */ 60 | public nextVector3D() { 61 | return new Vector3D(this.next(), this.next(), this.next()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/forces/solar-radiation-pressure.ts: -------------------------------------------------------------------------------- 1 | import { SunBody } from "../bodies/sun-body"; 2 | import { J2000 } from "../coordinates/j2000"; 3 | import { AccelerationForce, AccelerationMap } from "../forces/forces-interface"; 4 | 5 | /** Model of solar radiation pressure, for use in a ForceModel object. */ 6 | export class SolarRadiationPressure implements AccelerationForce { 7 | /** spacecraft mass, in kilograms */ 8 | private mass: number; 9 | /** spacecraft area, in square meters */ 10 | private area: number; 11 | /** reflectivity coefficient (unitless) */ 12 | private reflectCoeff: number; 13 | 14 | /** 15 | * Create a new solar radiation pressure AccelerationForce object. 16 | * 17 | * @param mass spacecraft mass, in kilograms 18 | * @param area spacecraft area, in square meters 19 | * @param reflectCoeff reflectivity coefficient (unitless) 20 | */ 21 | constructor(mass: number, area: number, reflectCoeff: number) { 22 | this.mass = mass; 23 | this.area = area; 24 | this.reflectCoeff = reflectCoeff; 25 | } 26 | 27 | /** 28 | * Calculate acceleration due to solar radiation pressure. 29 | * 30 | * @param j2kState J2000 state vector 31 | */ 32 | private radiationPressure(j2kState: J2000) { 33 | const { mass, area, reflectCoeff } = this; 34 | const rSun = SunBody.position(j2kState.epoch); 35 | const r = rSun.changeOrigin(j2kState.position); 36 | const pFac = -(SunBody.SOLAR_PRESSURE * reflectCoeff * area) / mass; 37 | return r.normalized().scale(pFac / 1000); 38 | } 39 | 40 | /** 41 | * Update the acceleration map argument with a calculated 42 | * "solar_radiation_pressure" value, for the provided state vector. 43 | * 44 | * @param j2kState J2000 state vector 45 | * @param accMap acceleration map (km/s^2) 46 | */ 47 | public acceleration(j2kState: J2000, accMap: AccelerationMap) { 48 | accMap["solar_radiation_pressure"] = this.radiationPressure(j2kState); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/math/spherical-harmonics.ts: -------------------------------------------------------------------------------- 1 | /** Class for handling spherical harmonics operations. */ 2 | export class SphericalHarmonics { 3 | /** dimension */ 4 | private d: number; 5 | /** associated legendre polynomial table */ 6 | private P: number[]; 7 | 8 | /** 9 | * Create a new SphericalHarmonics object. 10 | * 11 | * @param dimension degree 12 | */ 13 | constructor(dimension: number) { 14 | this.d = dimension; 15 | this.P = []; 16 | } 17 | 18 | private index(l: number, m: number) { 19 | return (l * l + l) / 2 + m; 20 | } 21 | 22 | /** 23 | * Fetch the associated legendre polynomial from the provided index. 24 | * 25 | * @param l l-index 26 | * @param m m-index 27 | */ 28 | public getP(l: number, m: number) { 29 | if (m > l) { 30 | return 0; 31 | } 32 | return this.P[this.index(l, m)] || 0; 33 | } 34 | 35 | /** Reset the polynomial table to zeroes. */ 36 | private clearTable() { 37 | this.P = []; 38 | for (let i = 0; i <= this.index(this.d, this.d); i++) { 39 | this.P.push(0); 40 | } 41 | } 42 | 43 | /** 44 | * Build a cache of associated Legendre polynomials. 45 | * 46 | * @param phi geocentric latitude 47 | */ 48 | public buildCache(phi: number) { 49 | this.clearTable(); 50 | const { d, P } = this; 51 | const sPhi = Math.sin(phi); 52 | const cPhi = Math.cos(phi); 53 | P[this.index(0, 0)] = 1; 54 | P[this.index(1, 0)] = sPhi; 55 | P[this.index(1, 1)] = cPhi; 56 | for (let l = 2; l <= d; l++) { 57 | for (let m = 0; m <= l; m++) { 58 | if (l >= 2 && m == 0) { 59 | P[this.index(l, 0)] = 60 | ((2 * l - 1) * sPhi * this.getP(l - 1, 0) - 61 | (l - 1) * this.getP(l - 2, 0)) / 62 | l; 63 | } else if (m != 0 && m < l) { 64 | P[this.index(l, m)] = 65 | this.getP(l - 2, m) + (2 * l - 1) * cPhi * this.getP(l - 1, m - 1); 66 | } else if (l == m && l != 0) { 67 | P[this.index(l, l)] = (2 * l - 1) * cPhi * this.getP(l - 1, l - 1); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/coordinates/geodetic.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { RAD2DEG } from "../math/constants"; 3 | import { Vector3D } from "../math/vector-3d"; 4 | import { EpochUTC } from "../time/epoch-utc"; 5 | import { ITRF } from "./itrf"; 6 | 7 | /** Class representing Geodetic (LLA) coordinates. */ 8 | export class Geodetic { 9 | /** Geodetic latitude, in radians. */ 10 | public readonly latitude: number; 11 | /** Geodetic longitude, in radians. */ 12 | public readonly longitude: number; 13 | /** Geodetic altitude, in kilometers. */ 14 | public readonly altitude: number; 15 | 16 | /** 17 | * Create a new Geodetic object. 18 | * 19 | * @param latitude geodetic latitude, in radians 20 | * @param longitude geodetic longitude, in radians 21 | * @param altitude geodetic altitude, in kilometers 22 | */ 23 | constructor(latitude: number, longitude: number, altitude: number) { 24 | this.latitude = latitude; 25 | this.longitude = longitude; 26 | this.altitude = altitude; 27 | } 28 | 29 | /** Return a string representation of the object. */ 30 | public toString(): string { 31 | const { latitude, longitude, altitude } = this; 32 | const output = [ 33 | "[Geodetic]", 34 | ` Latitude: ${(latitude * RAD2DEG).toFixed(3)}\u00b0`, 35 | ` Longitude: ${(longitude * RAD2DEG).toFixed(3)}\u00b0`, 36 | ` Altitude: ${altitude.toFixed(3)} km` 37 | ]; 38 | return output.join("\n"); 39 | } 40 | 41 | /** 42 | * Convert this to an ITRF coordinate object. 43 | * 44 | * @param epoch UTC epoch 45 | */ 46 | public toITRF(epoch: EpochUTC) { 47 | const { latitude, longitude, altitude } = this; 48 | const sLat = Math.sin(latitude); 49 | const cLat = Math.cos(latitude); 50 | const nVal = 51 | EarthBody.RADIUS_EQUATOR / 52 | Math.sqrt(1 - EarthBody.ECCENTRICITY_SQUARED * sLat * sLat); 53 | const rx = (nVal + altitude) * cLat * Math.cos(longitude); 54 | const ry = (nVal + altitude) * cLat * Math.sin(longitude); 55 | const rz = (nVal * (1 - EarthBody.ECCENTRICITY_SQUARED) + altitude) * sLat; 56 | return new ITRF(epoch, new Vector3D(rx, ry, rz)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/bodies/moon-body.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { DEG2RAD } from "../math/constants"; 3 | import { Vector3D } from "../math/vector-3d"; 4 | import { EpochUTC } from "../time/epoch-utc"; 5 | 6 | export class MoonBody { 7 | /** Moon gravitational parameter, in km^3/s^2. */ 8 | public static readonly MU = 4902.801; 9 | 10 | /** 11 | * Calculate the J2000 position of the Moon, in kilometers, at a given epoch. 12 | * 13 | * @param epoch satellite state epoch 14 | */ 15 | public static position(epoch: EpochUTC) { 16 | const jc = epoch.toUT1().toJulianCenturies(); 17 | const dtr = DEG2RAD; 18 | const lamEcl = 19 | 218.32 + 20 | 481267.883 * jc + 21 | 6.29 * Math.sin((134.9 + 477198.85 * jc) * dtr) - 22 | 1.27 * Math.sin((259.2 - 413335.38 * jc) * dtr) + 23 | 0.66 * Math.sin((235.7 + 890534.23 * jc) * dtr) + 24 | 0.21 * Math.sin((269.9 + 954397.7 * jc) * dtr) - 25 | 0.19 * Math.sin((357.5 + 35999.05 * jc) * dtr) - 26 | 0.11 * Math.sin((186.6 + 966404.05 * jc) * dtr); 27 | const phiEcl = 28 | 5.13 * Math.sin((93.3 + 483202.03 * jc) * dtr) + 29 | 0.28 * Math.sin((228.2 + 960400.87 * jc) * dtr) - 30 | 0.28 * Math.sin((318.3 + 6003.18 * jc) * dtr) - 31 | 0.17 * Math.sin((217.6 - 407332.2 * jc) * dtr); 32 | const pllx = 33 | 0.9508 + 34 | 0.0518 * Math.cos((134.9 + 477198.85 * jc) * dtr) + 35 | 0.0095 * Math.cos((259.2 - 413335.38 * jc) * dtr) + 36 | 0.0078 * Math.cos((235.7 + 890534.23 * jc) * dtr) + 37 | 0.0028 * Math.cos((269.9 + 954397.7 * jc) * dtr); 38 | const obq = 23.439291 - 0.0130042 * jc; 39 | const rMag = 1.0 / Math.sin(pllx * dtr); 40 | const rI = rMag * (Math.cos(phiEcl * dtr) * Math.cos(lamEcl * dtr)); 41 | const rJ = 42 | rMag * 43 | (Math.cos(obq * dtr) * Math.cos(phiEcl * dtr) * Math.sin(lamEcl * dtr) - 44 | Math.sin(obq * dtr) * Math.sin(phiEcl * dtr)); 45 | const rK = 46 | rMag * 47 | (Math.sin(obq * dtr) * Math.cos(phiEcl * dtr) * Math.sin(lamEcl * dtr) + 48 | Math.cos(obq * dtr) * Math.sin(phiEcl * dtr)); 49 | return new Vector3D(rI, rJ, rK).scale(EarthBody.RADIUS_EQUATOR); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/forces/atmospheric-drag.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { J2000 } from "../coordinates/j2000"; 3 | import { DataHandler } from "../data/data-handler"; 4 | import { AccelerationForce, AccelerationMap } from "./forces-interface"; 5 | 6 | /** Model of atmospheric drag, for use in a ForceModel object. */ 7 | export class AtmosphericDrag implements AccelerationForce { 8 | /** spacecraft mass, in kilograms */ 9 | private mass: number; 10 | /** spacecraft area, in square meters */ 11 | private area: number; 12 | /** drag coefficient (unitless) */ 13 | private dragCoeff: number; 14 | 15 | /** 16 | * Create a new atmospheric drag AccelerationForce object. 17 | * 18 | * @param mass spacecraft mass, in kilograms 19 | * @param area spacecraft area, in square meters 20 | * @param dragCoeff drag coefficient (unitless) 21 | */ 22 | constructor(mass: number, area: number, dragCoeff: number) { 23 | this.mass = mass; 24 | this.area = area; 25 | this.dragCoeff = dragCoeff; 26 | } 27 | 28 | /** 29 | * Calculate acceleration due to atmospheric drag, using the Exponential 30 | * Atmospheric Density model. 31 | * 32 | * @param j2kState J2000 state vector 33 | */ 34 | public expAtmosphereDrag(j2kState: J2000) { 35 | const { position, velocity } = j2kState; 36 | const { mass, area, dragCoeff } = this; 37 | var density = DataHandler.getExpAtmosphericDensity(position); 38 | var vRel = velocity 39 | .add( 40 | EarthBody.getRotation(j2kState.epoch) 41 | .negate() 42 | .cross(position) 43 | ) 44 | .scale(1000); 45 | var fScale = 46 | -0.5 * 47 | density * 48 | ((dragCoeff * area) / mass) * 49 | Math.pow(vRel.magnitude(), 2); 50 | return vRel.normalized().scale(fScale / 1000); 51 | } 52 | 53 | /** 54 | * Update the acceleration map argument with a calculated "atmospheric_drag" 55 | * value, for the provided state vector. 56 | * 57 | * @param j2kState J2000 state vector 58 | * @param accMap acceleration map (km/s^2) 59 | */ 60 | public acceleration(j2kState: J2000, accMap: AccelerationMap) { 61 | accMap["atmospheric_drag"] = this.expAtmosphereDrag(j2kState); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/math/vector-6d.ts: -------------------------------------------------------------------------------- 1 | import { Vector3D } from "./vector-3d"; 2 | 3 | /** Class representing a vector of length 6. */ 4 | export class Vector6D { 5 | /** Vector a-axis component. */ 6 | public readonly a: number; 7 | /** Vector b-axis component. */ 8 | public readonly b: number; 9 | /** Vector c-axis component. */ 10 | public readonly c: number; 11 | /** Vector x-axis component. */ 12 | public readonly x: number; 13 | /** Vector y-axis component. */ 14 | public readonly y: number; 15 | /** Vector z-axis component. */ 16 | public readonly z: number; 17 | 18 | /** 19 | * Create a new Vector6D object. 20 | * 21 | * @param a a-axis component 22 | * @param b b-axis component 23 | * @param c c-axis component 24 | * @param x x-axis component 25 | * @param y y-axis component 26 | * @param z z-axis component 27 | */ 28 | constructor( 29 | a: number, 30 | b: number, 31 | c: number, 32 | x: number, 33 | y: number, 34 | z: number 35 | ) { 36 | this.a = a; 37 | this.b = b; 38 | this.c = c; 39 | this.x = x; 40 | this.y = y; 41 | this.z = z; 42 | } 43 | 44 | /** 45 | * Create a new Vector6D object, containing zero for each state element. 46 | */ 47 | public static origin(): Vector6D { 48 | return new Vector6D(0, 0, 0, 0, 0, 0); 49 | } 50 | 51 | /** 52 | * Perform element-wise addition of this and another Vector. 53 | * 54 | * Returns a new Vector object containing the sum. 55 | * 56 | * @param v the other vector 57 | */ 58 | public add(v: Vector6D): Vector6D { 59 | const { a, b, c, x, y, z } = this; 60 | return new Vector6D(a + v.a, b + v.b, c + v.c, x + v.x, y + v.y, z + v.z); 61 | } 62 | 63 | /** 64 | * Linearly scale the elements of this. 65 | * 66 | * Returns a new Vector object containing the scaled state. 67 | * 68 | * @param n scalar value 69 | */ 70 | public scale(n: number): Vector6D { 71 | const { a, b, c, x, y, z } = this; 72 | return new Vector6D(a * n, b * n, c * n, x * n, y * n, z * n); 73 | } 74 | 75 | /** Split this into two Vector3D objects. */ 76 | public split(): [Vector3D, Vector3D] { 77 | const { a, b, c, x, y, z } = this; 78 | return [new Vector3D(a, b, c), new Vector3D(x, y, z)]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/coordinates/teme.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { Vector3D } from "../math/vector-3d"; 3 | import { EpochUTC } from "../time/epoch-utc"; 4 | import { IStateVector } from "./coordinate-interface"; 5 | import { J2000 } from "./j2000"; 6 | 7 | /** Class representing True Equator Mean Equinox (TEME) coordinates. */ 8 | export class TEME implements IStateVector { 9 | /** Satellite state epoch. */ 10 | public readonly epoch: EpochUTC; 11 | /** Position vector, in kilometers. */ 12 | public readonly position: Vector3D; 13 | /** Velocity vector, in kilometers per second. */ 14 | public readonly velocity: Vector3D; 15 | 16 | /** 17 | * Create a new TEME object. 18 | * 19 | * @param epoch UTC epoch 20 | * @param position J2000 position, in kilometers 21 | * @param velocity J2000 velocity, in kilometers per second 22 | */ 23 | constructor(epoch: EpochUTC, position: Vector3D, velocity: Vector3D) { 24 | this.epoch = epoch; 25 | this.position = position; 26 | this.velocity = velocity || Vector3D.origin(); 27 | } 28 | 29 | /** Return a string representation of this object. */ 30 | public toString(): string { 31 | const { epoch, position, velocity } = this; 32 | const output = [ 33 | "[TEME]", 34 | ` Epoch: ${epoch.toString()}`, 35 | ` Position: ${position.toString()} km`, 36 | ` Velocity: ${velocity.toString()} km/s` 37 | ]; 38 | return output.join("\n"); 39 | } 40 | 41 | /** Convert this to a TEME state vector object. */ 42 | public toJ2000() { 43 | const { epoch, position, velocity } = this; 44 | const [dPsi, dEps, mEps] = EarthBody.nutation(epoch); 45 | const epsilon = mEps + dEps; 46 | const rMOD = position 47 | .rot3(-dPsi * Math.cos(epsilon)) 48 | .rot1(epsilon) 49 | .rot3(dPsi) 50 | .rot1(-mEps); 51 | const vMOD = velocity 52 | .rot3(-dPsi * Math.cos(epsilon)) 53 | .rot1(epsilon) 54 | .rot3(dPsi) 55 | .rot1(-mEps); 56 | const [zeta, theta, zed] = EarthBody.precession(epoch); 57 | const rJ2K = rMOD 58 | .rot3(zed) 59 | .rot2(-theta) 60 | .rot3(zeta); 61 | const vJ2K = vMOD 62 | .rot3(zed) 63 | .rot2(-theta) 64 | .rot3(zeta); 65 | return new J2000(this.epoch, rJ2K, vJ2K); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/forces/third-body.ts: -------------------------------------------------------------------------------- 1 | import { MoonBody } from "../bodies/moon-body"; 2 | import { SunBody } from "../bodies/sun-body"; 3 | import { J2000 } from "../coordinates/j2000"; 4 | import { AccelerationForce, AccelerationMap } from "./forces-interface"; 5 | 6 | /** Model of third-body gravity, for use in a ForceModel object. */ 7 | export class ThirdBody implements AccelerationForce { 8 | /** model Moon gravity, if true */ 9 | private moonGravityFlag: boolean; 10 | /** model Sun gravity, if true */ 11 | private sunGravityFlag: boolean; 12 | 13 | /** 14 | * Create a new ThirdBody object. 15 | * 16 | * @param moonGravityFlag model Moon gravity, if true 17 | * @param sunGravityFlag model Sun gravity, if true 18 | */ 19 | constructor(moonGravityFlag: boolean, sunGravityFlag: boolean) { 20 | this.moonGravityFlag = moonGravityFlag; 21 | this.sunGravityFlag = sunGravityFlag; 22 | } 23 | 24 | /** 25 | * Calculate acceleration due to the Moon's gravity. 26 | * 27 | * @param j2kState J2000 state vector 28 | */ 29 | private moonGravity(j2kState: J2000) { 30 | const rMoon = MoonBody.position(j2kState.epoch); 31 | const aNum = rMoon.changeOrigin(j2kState.position); 32 | const aDen = Math.pow(aNum.magnitude(), 3); 33 | const bNum = rMoon; 34 | const bDen = Math.pow(rMoon.magnitude(), 3); 35 | const grav = aNum.scale(1.0 / aDen).add(bNum.scale(-1.0 / bDen)); 36 | return grav.scale(MoonBody.MU); 37 | } 38 | 39 | /** 40 | * Calculate acceleration due to the Sun's gravity. 41 | * 42 | * @param j2kState J2000 state vector 43 | */ 44 | private sunGravity(j2kState: J2000) { 45 | const rSun = SunBody.position(j2kState.epoch); 46 | const aNum = rSun.changeOrigin(j2kState.position); 47 | const aDen = Math.pow(aNum.magnitude(), 3); 48 | const bNum = rSun; 49 | const bDen = Math.pow(rSun.magnitude(), 3); 50 | const grav = aNum.scale(1.0 / aDen).add(bNum.scale(-1.0 / bDen)); 51 | return grav.scale(SunBody.MU); 52 | } 53 | 54 | /** 55 | * Update the acceleration map argument with calculated "moon_gravity" and 56 | * "sun_gravity" values, for the provided state vector. 57 | * 58 | * @param j2kState J2000 state vector 59 | * @param accMap acceleration map (km/s^2) 60 | */ 61 | public acceleration(j2kState: J2000, accMap: AccelerationMap) { 62 | if (this.moonGravityFlag) { 63 | accMap["moon_gravity"] = this.moonGravity(j2kState); 64 | } 65 | if (this.sunGravityFlag) { 66 | accMap["sun_gravity"] = this.sunGravity(j2kState); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/bodies/sun-body.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { J2000 } from "../coordinates/j2000"; 3 | import { ASTRONOMICAL_UNIT, DEG2RAD, SPEED_OF_LIGHT } from "../math/constants"; 4 | import { Vector3D } from "../math/vector-3d"; 5 | import { EpochUTC } from "../time/epoch-utc"; 6 | 7 | export class SunBody { 8 | /** Moon gravitational parameter, in km^3/s^2. */ 9 | public static readonly MU = 132712440017.987; 10 | 11 | /** Solar flux, in W/m^2 */ 12 | public static readonly SOLAR_FLUX = 1353; 13 | 14 | /** Solar Radiation Pressure, in N/m^2. */ 15 | public static readonly SOLAR_PRESSURE = SunBody.SOLAR_FLUX / SPEED_OF_LIGHT; 16 | 17 | /** Solar umbra angle, in radians. */ 18 | public static readonly UMBRA_ANGLE = 0.26411888 * DEG2RAD; 19 | 20 | /** Solar penumbra angle, in radians. */ 21 | public static readonly PENUMBRA_ANGLE = 0.26900424 * DEG2RAD; 22 | 23 | /** 24 | * Calculate the J2000 position of the Sun, in kilometers, at a given epoch. 25 | * 26 | * @param epoch satellite state epoch 27 | */ 28 | public static position(epoch: EpochUTC) { 29 | const jc = epoch.toUT1().toJulianCenturies(); 30 | const dtr = DEG2RAD; 31 | const lamSun = 280.46 + 36000.77 * jc; 32 | const mSun = 357.5277233 + 35999.05034 * jc; 33 | const lamEc = 34 | lamSun + 35 | 1.914666471 * Math.sin(mSun * dtr) + 36 | 0.019994643 * Math.sin(2 * mSun * dtr); 37 | const obliq = 23.439291 - 0.0130042 * jc; 38 | const rMag = 39 | 1.000140612 - 40 | 0.016708617 * Math.cos(mSun * dtr) - 41 | 0.000139589 * Math.cos(2 * mSun * dtr); 42 | const rI = rMag * Math.cos(lamEc * dtr); 43 | const rJ = rMag * Math.cos(obliq * dtr) * Math.sin(lamEc * dtr); 44 | const rK = rMag * Math.sin(obliq * dtr) * Math.sin(lamEc * dtr); 45 | return new Vector3D(rI, rJ, rK).scale(ASTRONOMICAL_UNIT); 46 | } 47 | 48 | /** 49 | * Return true if argument state is in eclipse. 50 | * 51 | * @param j2kState J2000 state vector 52 | */ 53 | public static shadow(j2kState: J2000) { 54 | const { epoch, position: posSat } = j2kState; 55 | const posSun = SunBody.position(epoch); 56 | let shadow = false; 57 | if (posSun.dot(posSat) < 0) { 58 | const angle = posSun.angle(posSat); 59 | const r = posSat.magnitude(); 60 | const satHoriz = r * Math.cos(angle); 61 | const satVert = r * Math.sin(angle); 62 | const penVert = 63 | EarthBody.RADIUS_EQUATOR + Math.tan(SunBody.PENUMBRA_ANGLE) * satHoriz; 64 | if (satVert <= penVert) { 65 | shadow = true; 66 | } 67 | } 68 | return shadow; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/bodies-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { 3 | EarthBody, 4 | EpochUTC, 5 | J2000, 6 | MoonBody, 7 | RungeKutta4Propagator, 8 | SunBody, 9 | Vector3D 10 | } from "../index"; 11 | 12 | const epoch = EpochUTC.fromDateString("2018-12-21T00:00:00.000Z"); 13 | 14 | const state = new J2000( 15 | epoch, 16 | new Vector3D(-1117.913276, 73.093299, -7000.018272), 17 | new Vector3D(3.531365461, 6.583914964, -0.495649656) 18 | ); 19 | 20 | const rk4Prop = new RungeKutta4Propagator(state); 21 | 22 | describe("MoonBody", () => { 23 | describe("position", () => { 24 | const actual = MoonBody.position(epoch); 25 | const expected = new Vector3D( 26 | 154366.09642497, 27 | 318375.615233499, 28 | 109213.672184026 29 | ); 30 | it("should be within 300km of real-world magnitude", () => { 31 | const magnitude = Math.abs(expected.magnitude() - actual.magnitude()); 32 | assert(magnitude <= 300); 33 | }); 34 | it("should be within 0.25 degrees of real-world angle", () => { 35 | const angle = expected.angle(actual) * (180 / Math.PI); 36 | assert(angle <= 0.25); 37 | }); 38 | }); 39 | }); 40 | 41 | describe("SunBody", () => { 42 | describe("position", () => { 43 | const actual = SunBody.position(epoch); 44 | const expected = new Vector3D( 45 | -3092558.657913523, 46 | -134994294.84136814, 47 | -58520244.455122419 48 | ); 49 | it("should be within 7000km of real-world magnitude", () => { 50 | const magnitude = Math.abs(expected.magnitude() - actual.magnitude()); 51 | assert(magnitude <= 7000); 52 | }); 53 | it("should be within 0.30 degrees of real-world angle", () => { 54 | const angle = expected.angle(actual) * (180 / Math.PI); 55 | assert(angle <= 0.3); 56 | }); 57 | }); 58 | 59 | describe("shadow", () => { 60 | rk4Prop.reset(); 61 | rk4Prop.setStepSize(30); 62 | rk4Prop.forceModel.clearModel(); 63 | rk4Prop.forceModel.setEarthGravity(0, 0); 64 | let propEpoch = epoch; 65 | let errorCount = 0; 66 | it("should approximately match inverted vector sight algorithm", () => { 67 | for (let i = 0; i < 1440; i++) { 68 | const satState = rk4Prop.propagate(propEpoch); 69 | const shadow = SunBody.shadow(satState); 70 | const satPos = satState.position; 71 | const sunPos = SunBody.position(propEpoch); 72 | const sight = satPos.sight(sunPos, EarthBody.RADIUS_MEAN); 73 | if (shadow === sight) { 74 | errorCount++; 75 | } 76 | propEpoch = propEpoch.roll(60); 77 | } 78 | assert(errorCount <= 3); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/time/epoch-utc.ts: -------------------------------------------------------------------------------- 1 | import { DataHandler } from "../data/data-handler"; 2 | import { DEG2RAD, TWO_PI } from "../math/constants"; 3 | import { evalPoly } from "../math/operations"; 4 | import { AbstractEpoch } from "./abstract-epoch"; 5 | import { EpochTAI, EpochTDB, EpochTT, EpochUT1 } from "./time-scales"; 6 | 7 | /** Class representing a UTC astrodynamic epoch. */ 8 | export class EpochUTC extends AbstractEpoch { 9 | /** 10 | * Create a new Epoch object. 11 | * 12 | * @param millis milliseconds since 1 January 1970, 00:00 UTC 13 | */ 14 | constructor(millis: number) { 15 | super(millis); 16 | } 17 | 18 | /** 19 | * Create a new EpochUTC object from a valid JavaScript Date string. 20 | * 21 | * @param dateStr 22 | */ 23 | public static fromDateString(dateStr: string) { 24 | return new EpochUTC(new Date(dateStr).getTime()); 25 | } 26 | 27 | /** Return a new Epoch object, containing current time. */ 28 | public static now(): EpochUTC { 29 | return new EpochUTC(new Date().getTime()); 30 | } 31 | 32 | /** 33 | * Return a new Epoch, incremented by some desired number of seconds. 34 | * 35 | * @param seconds seconds to increment (can be negative) 36 | */ 37 | public roll(seconds: number): EpochUTC { 38 | return new EpochUTC((this.unix + seconds) * 1000); 39 | } 40 | 41 | /** Convert to UNSO Modified Julian Date. */ 42 | public toMjd() { 43 | return this.toJulianDate() - 2400000.5; 44 | } 45 | 46 | /** Convert to GSFC Modified Julian Date. */ 47 | public toMjdGsfc() { 48 | return this.toJulianDate() - 2400000.5 - 29999.5; 49 | } 50 | 51 | /** Convert to a UT1 Epoch. */ 52 | public toUT1() { 53 | const { dut1 } = DataHandler.getFinalsData(this.toMjd()); 54 | return new EpochUT1((this.unix + dut1) * 1000); 55 | } 56 | 57 | /** Convert to an International Atomic Time (TAI) Epoch. */ 58 | public toTAI() { 59 | const ls = DataHandler.leapSecondsOffset(this.toJulianDate()); 60 | return new EpochTAI((this.unix + ls) * 1000); 61 | } 62 | 63 | /** Convert to a Terrestrial Time (TT) Epoch. */ 64 | public toTT() { 65 | return new EpochTT((this.toTAI().unix + 32.184) * 1000); 66 | } 67 | 68 | /** Convert to a Barycentric Dynamical Time (TDB) Epoch. */ 69 | public toTDB() { 70 | const tt = this.toTT(); 71 | const tTT = tt.toJulianCenturies(); 72 | const mEarth = (357.5277233 + 35999.05034 * tTT) * DEG2RAD; 73 | const seconds = 74 | 0.001658 * Math.sin(mEarth) + 0.00001385 * Math.sin(2 * mEarth); 75 | return new EpochTDB((tt.unix + seconds) * 1000); 76 | } 77 | 78 | /** 79 | * Calculate the Greenwich Mean Sideral Time (GMST) angle for this epoch, 80 | * in radians. 81 | */ 82 | public gmstAngle() { 83 | const t = this.toUT1().toJulianCenturies(); 84 | const seconds = evalPoly(t, [ 85 | 67310.54841, 86 | 876600.0 * 3600.0 + 8640184.812866, 87 | 0.093104, 88 | 6.2e-6 89 | ]); 90 | return ((seconds % 86400) / 86400) * TWO_PI; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/propagators/kepler-propagator.ts: -------------------------------------------------------------------------------- 1 | import { ClassicalElements } from "../coordinates/classical-elements"; 2 | import { J2000 } from "../coordinates/j2000"; 3 | import { TWO_PI } from "../math/constants"; 4 | import { matchHalfPlane } from "../math/operations"; 5 | import { EpochUTC } from "../time/epoch-utc"; 6 | import { IPropagator } from "./propagator-interface"; 7 | 8 | /** Satellite ephemeris propagator, using Kepler's method. */ 9 | export class KeplerPropagator implements IPropagator { 10 | /** Cache for last computed statellite state. */ 11 | public state: J2000; 12 | /** Keplerian element set. */ 13 | private readonly elements: ClassicalElements; 14 | 15 | /** 16 | * Create a new Kepler propagator object. This propagator only models 17 | * two-body effects on the orbiting object. 18 | * 19 | * @param elements Keplerian element set 20 | */ 21 | constructor(elements: ClassicalElements) { 22 | this.elements = elements; 23 | this.state = elements.toJ2000(); 24 | } 25 | 26 | /** Return a string representation of the object. */ 27 | public toString() { 28 | return ["[Kepler]", " Two-Body Propagator"].join("\n"); 29 | } 30 | 31 | /** 32 | * Restore cached state to initial propagator state. Doesn't really do much 33 | * for the Kepler propagator, since it doesn't rely on transient states. 34 | */ 35 | public reset(): KeplerPropagator { 36 | this.state = this.elements.toJ2000(); 37 | return this; 38 | } 39 | 40 | /** 41 | * Propagate satellite state to a new epoch. 42 | * 43 | * @param epoch UTC epoch 44 | */ 45 | public propagate(epoch: EpochUTC): J2000 { 46 | const { epoch: t, a, e, i, o, w, v } = this.elements; 47 | const delta = epoch.difference(t); 48 | const n = this.elements.meanMotion(); 49 | let eaInit = Math.acos((e + Math.cos(v)) / (1 + e * Math.cos(v))); 50 | eaInit = matchHalfPlane(eaInit, v); 51 | let maInit = eaInit - e * Math.sin(eaInit); 52 | maInit = matchHalfPlane(maInit, eaInit); 53 | const maFinal = (maInit + n * delta) % TWO_PI; 54 | let eaFinal = maFinal; 55 | for (let iter = 0; iter < 32; iter++) { 56 | const eaTemp = maFinal + e * Math.sin(eaFinal); 57 | if (Math.abs(eaTemp - eaFinal) < 1e-12) { 58 | break; 59 | } 60 | eaFinal = eaTemp; 61 | } 62 | let vFinal = Math.acos( 63 | (Math.cos(eaFinal) - e) / (1 - e * Math.cos(eaFinal)) 64 | ); 65 | vFinal = matchHalfPlane(vFinal, eaFinal); 66 | this.state = new ClassicalElements(epoch, a, e, i, o, w, vFinal).toJ2000(); 67 | return this.state; 68 | } 69 | 70 | /** 71 | * Propagate state by some number of seconds, repeatedly, starting at a 72 | * specified epoch. 73 | * 74 | * @param epoch UTC epoch 75 | * @param interval seconds between output states 76 | * @param count number of steps to take 77 | */ 78 | public step(epoch: EpochUTC, interval: number, count: number): J2000[] { 79 | const output: J2000[] = [this.propagate(epoch)]; 80 | let tempEpoch = epoch; 81 | for (let i = 0; i < count; i++) { 82 | tempEpoch = tempEpoch.roll(interval); 83 | output.push(this.propagate(tempEpoch)); 84 | } 85 | return output; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/propagator-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { DataHandler } from "../data/data-handler"; 3 | import { 4 | EpochUTC, 5 | J2000, 6 | KeplerPropagator, 7 | RungeKutta4Propagator, 8 | Vector3D 9 | } from "../index"; 10 | import { InterpolatorPropagator } from "../propagators/interpolator-propagator"; 11 | 12 | DataHandler.setFinalsData([ 13 | "181220 58472.00 I 0.111682 0.000013 0.267043 0.000011 I-0.0268992 0.0000042 0.8116 0.0029 I -107.187 1.028 -6.992 0.043", 14 | "181221 58473.00 I 0.109899 0.000013 0.266778 0.000012 I-0.0276299 0.0000041 0.6550 0.0031 I -107.216 1.028 -6.947 0.043", 15 | "181222 58474.00 I 0.107885 0.000012 0.266558 0.000012 I-0.0282132 0.0000046 0.5105 0.0030 I -107.320 1.028 -7.168 0.043" 16 | ]); 17 | 18 | const state = new J2000( 19 | EpochUTC.fromDateString("2018-12-21T00:00:00.000Z"), 20 | new Vector3D(-1117.913276, 73.093299, -7000.018272), 21 | new Vector3D(3.531365461, 6.583914964, -0.495649656) 22 | ); 23 | 24 | const epoch = EpochUTC.fromDateString("2018-12-22T00:00:00.000Z"); 25 | 26 | const rk4Prop = new RungeKutta4Propagator(state); 27 | const kepProp = new KeplerPropagator(state.toClassicalElements()); 28 | 29 | describe("KeplerPropagator", () => { 30 | describe("two-body", () => { 31 | rk4Prop.reset(); 32 | rk4Prop.setStepSize(10); 33 | rk4Prop.forceModel.clearModel(); 34 | rk4Prop.forceModel.setEarthGravity(0, 0); 35 | kepProp.reset(); 36 | const rk4Result = rk4Prop.propagate(epoch).position; 37 | const kepResult = kepProp.propagate(epoch).position; 38 | it("should be within 1m of numerical two-body after 24 hours", () => { 39 | assert(kepResult.distance(rk4Result) < 0.001); 40 | }); 41 | }); 42 | }); 43 | 44 | describe("RungeKutta4Propagator", () => { 45 | describe("high-accuracy", () => { 46 | rk4Prop.reset(); 47 | rk4Prop.setStepSize(10); 48 | rk4Prop.forceModel.clearModel(); 49 | rk4Prop.forceModel.setEarthGravity(50, 50); 50 | rk4Prop.forceModel.setThirdBody(true, true); 51 | const actual = rk4Prop.propagate(epoch).position; 52 | const expected = new Vector3D(-212.125533, -2464.351601, 6625.907454); 53 | it("should be within 25m of real-world ephemeris after 24 hours", () => { 54 | assert(expected.distance(actual) < 0.025); 55 | }); 56 | }); 57 | }); 58 | 59 | describe("InterpolatorPropagator", () => { 60 | describe("interpolate", () => { 61 | const rk4Prop = new RungeKutta4Propagator(state); 62 | rk4Prop.setStepSize(5); 63 | rk4Prop.forceModel.setEarthGravity(50, 50); 64 | rk4Prop.forceModel.setThirdBody(true, true); 65 | const cacheDense = rk4Prop.step(state.epoch, 60, 86400 / 60); 66 | rk4Prop.reset(); 67 | const cacheSparse = rk4Prop.step(state.epoch, 900, 86400 / 900); 68 | const interpolator = new InterpolatorPropagator(cacheSparse); 69 | interpolator.forceModel.setEarthGravity(2, 0); 70 | let maxError = 0; 71 | for (let cState of cacheDense) { 72 | const iState = interpolator.propagate(cState.epoch); 73 | const dist = cState.position.distance(iState.position); 74 | maxError = Math.max(maxError, dist); 75 | } 76 | it("should have a maximum error of 30m", () => { 77 | assert(maxError < 0.03); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/math/operations.ts: -------------------------------------------------------------------------------- 1 | import { TWO_PI } from "./constants"; 2 | 3 | /** 4 | * Calculate the factorial of a number. 5 | * 6 | * Throws an error if argument is not a positive integer. 7 | * 8 | * @param n a positive integer 9 | */ 10 | export function factorial(n: number): number { 11 | n = Math.abs(n); 12 | let output = 1; 13 | for (let i = 0; i < n; i++) { 14 | output *= i + 1; 15 | } 16 | return output; 17 | } 18 | 19 | /** 20 | * Evaluate a polynomial given a variable and its coefficients. Exponents are 21 | * implied to start at zero. 22 | * 23 | * @param x variable 24 | * @param coeffs coefficients, from lowest to highest 25 | */ 26 | export function evalPoly(x: number, coeffs: number[]): number { 27 | let output = 0; 28 | for (let n = 0; n < coeffs.length; n++) { 29 | output += coeffs[n] * x ** n; 30 | } 31 | return output; 32 | } 33 | 34 | /** 35 | * Return the sign of the number, 1 if positive, -1 if negative, 0 if zero. 36 | * 37 | * @param n a number 38 | */ 39 | export function sign(n: number): number { 40 | if (n < 0) { 41 | return -1; 42 | } 43 | if (n > 0) { 44 | return 1; 45 | } 46 | return 0; 47 | } 48 | 49 | /** 50 | * Return the angle (original or inverse) that exists in the half plane of the 51 | * match argument. 52 | * 53 | * @param angle angle to (possibly) adjust 54 | * @param match reference angle 55 | */ 56 | export function matchHalfPlane(angle: number, match: number): number { 57 | const [a1, a2] = [angle, TWO_PI - angle]; 58 | const d1 = Math.atan2(Math.sin(a1 - match), Math.cos(a1 - match)); 59 | const d2 = Math.atan2(Math.sin(a2 - match), Math.cos(a2 - match)); 60 | return Math.abs(d1) < Math.abs(d2) ? a1 : a2; 61 | } 62 | 63 | /** 64 | * Linearly interpolate between two known points. 65 | * 66 | * @param x value to interpolate 67 | * @param x0 start x-value 68 | * @param y0 start y-value 69 | * @param x1 end x-value 70 | * @param y1 end y-value 71 | */ 72 | export function linearInterpolate( 73 | x: number, 74 | x0: number, 75 | y0: number, 76 | x1: number, 77 | y1: number 78 | ): number { 79 | return (y0 * (x1 - x) + y1 * (x - x0)) / (x1 - x0); 80 | } 81 | 82 | /** 83 | * Copy the sign of one number, to the magnitude of another. 84 | * 85 | * @param magnitude 86 | * @param sign 87 | */ 88 | export function copySign(magnitude: number, sign: number) { 89 | const m = Math.abs(magnitude); 90 | const s = sign >= 0 ? 1 : -1; 91 | return s * m; 92 | } 93 | 94 | /** 95 | * Calculate the mean of the input array. 96 | * 97 | * @param values an array of numbers 98 | */ 99 | export function mean(values: number[]) { 100 | const n = values.length; 101 | let sum = 0; 102 | for (let i = 0; i < n; i++) { 103 | sum += values[i]; 104 | } 105 | return sum / n; 106 | } 107 | 108 | /** 109 | * Calculate the standard deviation of the input array. 110 | * 111 | * @param values an array of numbers 112 | */ 113 | export function standardDeviation(values: number[]) { 114 | const mu = mean(values); 115 | const n = values.length; 116 | let sum = 0; 117 | for (let i = 0; i < n; i++) { 118 | const sub = values[i] - mu; 119 | sum += sub * sub; 120 | } 121 | return Math.sqrt((1 / n) * sum); 122 | } 123 | -------------------------------------------------------------------------------- /src/examples/conjunction-example.ts: -------------------------------------------------------------------------------- 1 | import { ConjunctionAssesment, Matrix3D, Vector3D } from "../index"; 2 | 3 | // ============================================================================= 4 | // simulate conjunction using Conjunction Summary Message (CSM) data 5 | // example data from: https://www.space-track.org/documents/CSM_Guide.pdf 6 | // ============================================================================= 7 | 8 | // load relative position and covariance from the report 9 | const posA = new Vector3D(27.4, -70.2, 711.8); // meters 10 | const covA = new Matrix3D( // asset covariance (meters) 11 | new Vector3D(4.142e1, -8.579, -2.312e1), 12 | new Vector3D(-8.579, 2.533e3, 1.336e1), 13 | new Vector3D(-2.312e1, 1.336e1, 7.098e1) 14 | ); 15 | 16 | const posB = Vector3D.origin(); // zero since relative position is used 17 | const covB = new Matrix3D( // satellite covariance (meters) 18 | new Vector3D(1.337e3, -4.806e4, -3.298e1), 19 | new Vector3D(-4.806e4, 2.492e6, -7.588e2), 20 | new Vector3D(-3.298e1, -7.588e2, 7.105e1) 21 | ); 22 | 23 | // run a monte-carlo simulation of the conjunction and generate miss-distances 24 | const missDists = ConjunctionAssesment.simulateConjunctionMessage( 25 | posA, // asset position 26 | covA, // asset covariance 27 | posB, // satellite position 28 | covB, // satellite covariance 29 | 3, // standard deviation (sigma) 30 | 1000000 // iterations (some big number) 31 | ); 32 | 33 | // ============================================================================= 34 | // NOTE: results may vary due to the random nature of monte-carlo simulation 35 | // ============================================================================= 36 | 37 | console.log(`Expected Miss-Distance: ${posA.distance(posB).toFixed(3)} meters`); 38 | // => Expected Miss-Distance: 715.778 meters 39 | 40 | console.log( 41 | `Smallest Simulated Miss-Distance: ${missDists 42 | .reduce((a, b) => (a < b ? a : b)) 43 | .toFixed(3)} meters` 44 | ); 45 | // => Smallest Simulated Miss-Distance: 633.235 meters 46 | 47 | console.log( 48 | `Largest Simulated Miss-Distance: ${missDists 49 | .reduce((a, b) => (a > b ? a : b)) 50 | .toFixed(3)} meters` 51 | ); 52 | // => Largest Simulated Miss-Distance: 13121.900 meters 53 | 54 | // generate a histogram of the miss distances (for illustrative purposes) 55 | console.log("\n----- Miss-Distance Probability -----"); 56 | const step = 250; 57 | for (let m = 0; m <= 2500; m += step) { 58 | const count = missDists.filter(dist => dist >= m && dist < m + step).length; 59 | const percent = (count / missDists.length) * 100; 60 | const hist: string[] = ["|"]; 61 | for (let i = 0; i < percent; i++) { 62 | hist.push("*"); 63 | } 64 | const rangeStr = (m.toLocaleString() + "m: ").substr(0, 7); 65 | const percentStr = (percent.toFixed(1) + "% ").substr(0, 5); 66 | console.log(` ${rangeStr} ${percentStr} ${hist.join("")}`); 67 | } 68 | // => 69 | // ----- Miss-Distance Probability ----- 70 | // 0m: 0.0% | 71 | // 250m: 0.0% | 72 | // 500m: 6.4% |******* 73 | // 750m: 13.8% |************** 74 | // 1,000m: 9.0% |********* 75 | // 1,250m: 7.8% |******** 76 | // 1,500m: 7.0% |******** 77 | // 1,750m: 6.4% |******* 78 | // 2,000m: 5.9% |****** 79 | // 2,250m: 5.5% |****** 80 | // 2,500m: 5.0% |***** 81 | -------------------------------------------------------------------------------- /src/test/coordinates-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { TEME } from "../coordinates/teme"; 3 | import { DataHandler } from "../data/data-handler"; 4 | import { EpochUTC, ITRF, J2000, Vector3D } from "../index"; 5 | 6 | DataHandler.setFinalsData([ 7 | " 4 4 5 53100.00 I -0.141198 0.000079 0.331215 0.000051 I-0.4384012 0.0000027 1.5611 0.0020 I -52.007 .409 -4.039 .198 -0.141110 0.330940 -0.4383520 -52.100 -4.100", 8 | " 4 4 6 53101.00 I -0.140722 0.000071 0.333536 0.000057 I-0.4399498 0.0000028 1.5244 0.0019 I -52.215 .380 -3.846 .166 -0.140720 0.333270 -0.4399620 -52.500 -4.000", 9 | " 4 4 7 53102.00 I -0.140160 0.000067 0.336396 0.000060 I-0.4414071 0.0000026 1.3591 0.0024 I -52.703 .380 -3.878 .166 -0.140070 0.336140 -0.4414210 -52.700 -4.100" 10 | ]); 11 | 12 | const j2kState = new J2000( 13 | EpochUTC.fromDateString("2004-04-06T07:51:28.386Z"), 14 | new Vector3D(5102.5096, 6123.01152, 6378.1363), 15 | new Vector3D(-4.7432196, 0.7905366, 5.53375619) 16 | ); 17 | 18 | const itrfState = new ITRF( 19 | EpochUTC.fromDateString("2004-04-06T07:51:28.386Z"), 20 | new Vector3D(-1033.479383, 7901.2952758, 6380.3565953), 21 | new Vector3D(-3.22563652, -2.87245145, 5.531924446) 22 | ); 23 | 24 | const temeState = new TEME( 25 | EpochUTC.fromDateString("2017-04-28T12:45:00.000Z"), 26 | new Vector3D(12850.591182156, -40265.812871482, -282.022494587), 27 | new Vector3D(5.860360679, 1.871425143, -0.077719199) 28 | ); 29 | 30 | describe("J2000", () => { 31 | const testState = j2kState.toITRF(); 32 | it("should convert to ITRF within 0.6m", () => { 33 | const rDist = itrfState.position.distance(testState.position) * 1000; 34 | assert(rDist <= 0.6); 35 | }); 36 | it("should convert to ITRF within 0.0004m/s", () => { 37 | const vDist = itrfState.velocity.distance(testState.velocity) * 1000; 38 | assert(vDist <= 0.0004); 39 | }); 40 | }); 41 | 42 | describe("ITRF", () => { 43 | const testState = itrfState.toJ2000(); 44 | it("should convert to J2000 within 0.6m", () => { 45 | const rDist = j2kState.position.distance(testState.position) * 1000; 46 | assert(rDist <= 0.6); 47 | }); 48 | it("should convert to J2000 within 0.0004m/s", () => { 49 | const vDist = j2kState.velocity.distance(testState.velocity) * 1000; 50 | assert(vDist <= 0.0004); 51 | }); 52 | }); 53 | 54 | describe("TEME", () => { 55 | const testState = temeState.toJ2000(); 56 | const expected = new J2000( 57 | EpochUTC.fromDateString("2017-04-28T12:45:00.000Z"), 58 | new Vector3D(12694.023495137, -40315.279590286, -304.820736794), 59 | new Vector3D(5.867428953, 1.848712469, -0.087403192) 60 | ); 61 | it("should convert to J2000 within 0.3m", () => { 62 | const rDist = expected.position.distance(testState.position) * 1000; 63 | assert(rDist <= 0.3); 64 | }); 65 | it("should convert to J2000 within 0.00005m/s", () => { 66 | const vDist = expected.velocity.distance(testState.velocity) * 1000; 67 | assert(vDist <= 0.00005); 68 | }); 69 | const convBack = temeState.toJ2000().toTEME(); 70 | it("should convert back to J2000 within 1e-8m.", () => { 71 | const rDist = temeState.position.distance(convBack.position) * 1000; 72 | assert(rDist <= 1e-8); 73 | }); 74 | it("should convert back to J2000 within 1e-11m/s.", () => { 75 | const vDist = temeState.velocity.distance(convBack.velocity) * 1000; 76 | assert(vDist <= 1e-11); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/coordinates/classical-elements.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { RAD2DEG, TWO_PI } from "../math/constants"; 3 | import { Vector3D } from "../math/vector-3d"; 4 | import { EpochUTC } from "../time/epoch-utc"; 5 | import { J2000 } from "./j2000"; 6 | 7 | /** Class representing Keplerian orbital elements. */ 8 | export class ClassicalElements { 9 | /** Satellite state epoch. */ 10 | public readonly epoch: EpochUTC; 11 | /** Semimajor axis, in kilometers. */ 12 | public readonly a: number; 13 | /** Orbit eccentricity (unitless). */ 14 | public readonly e: number; 15 | /** Inclination, in radians. */ 16 | public readonly i: number; 17 | /** Right ascension of the ascending node, in radians. */ 18 | public readonly o: number; 19 | /** Argument of perigee, in radians. */ 20 | public readonly w: number; 21 | /** True anomaly, in radians. */ 22 | public readonly v: number; 23 | 24 | /** 25 | * Create a new Keplerian object. 26 | * 27 | * @param epoch UTC epoch 28 | * @param a semimajor axis, in kilometers 29 | * @param e orbit eccentricity (unitless) 30 | * @param i inclination, in radians 31 | * @param o right ascension of the ascending node, in radians 32 | * @param w argument of perigee, in radians 33 | * @param v true anomaly, in radians 34 | */ 35 | constructor( 36 | epoch: EpochUTC, 37 | a: number, 38 | e: number, 39 | i: number, 40 | o: number, 41 | w: number, 42 | v: number 43 | ) { 44 | this.epoch = epoch; 45 | this.a = a; 46 | this.e = e; 47 | this.i = i; 48 | this.o = o; 49 | this.w = w; 50 | this.v = v; 51 | } 52 | 53 | /** Return a string representation of the object. */ 54 | public toString() { 55 | const { epoch, a, e, i, o, w, v } = this; 56 | const epochOut = epoch.toString(); 57 | const aOut = a.toFixed(3); 58 | const eOut = e.toFixed(6); 59 | const iOut = (i * RAD2DEG).toFixed(4); 60 | const oOut = (o * RAD2DEG).toFixed(4); 61 | const wOut = (w * RAD2DEG).toFixed(4); 62 | const vOut = (v * RAD2DEG).toFixed(4); 63 | return [ 64 | "[KeplerianElements]", 65 | ` Epoch: ${epochOut}`, 66 | ` (a) Semimajor Axis: ${aOut} km`, 67 | ` (e) Eccentricity: ${eOut}`, 68 | ` (i) Inclination: ${iOut}\u00b0`, 69 | ` (\u03a9) Right Ascension: ${oOut}\u00b0`, 70 | ` (\u03c9) Argument of Perigee: ${wOut}\u00b0`, 71 | ` (\u03bd) True Anomaly: ${vOut}\u00b0` 72 | ].join("\n"); 73 | } 74 | 75 | /** Calculate the satellite's mean motion, in radians per second. */ 76 | public meanMotion(): number { 77 | return Math.sqrt(EarthBody.MU / this.a ** 3); 78 | } 79 | 80 | /** Calculate the number of revolutions the satellite completes per day. */ 81 | public revsPerDay(): number { 82 | return this.meanMotion() * (86400 / TWO_PI); 83 | } 84 | 85 | /** Convert this to a J2000 state vector object. */ 86 | public toJ2000() { 87 | const { epoch, a, e, i, o, w, v } = this; 88 | const { cos, sin, sqrt } = Math; 89 | const rPQW = new Vector3D(cos(v), sin(v), 0).scale( 90 | (a * (1 - e ** 2)) / (1 + e * cos(v)) 91 | ); 92 | const vPQW = new Vector3D(-sin(v), e + cos(v), 0).scale( 93 | sqrt(EarthBody.MU / (a * (1 - e ** 2))) 94 | ); 95 | const rJ2k = rPQW 96 | .rot3(-w) 97 | .rot1(-i) 98 | .rot3(-o); 99 | const vJ2k = vPQW 100 | .rot3(-w) 101 | .rot1(-i) 102 | .rot3(-o); 103 | return new J2000(epoch, rJ2k, vJ2k); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/coordinates/ric.ts: -------------------------------------------------------------------------------- 1 | import { Vector3D } from "../math/vector-3d"; 2 | import { EpochUTC } from "../time/epoch-utc"; 3 | import { IStateVector } from "./coordinate-interface"; 4 | import { J2000 } from "./j2000"; 5 | import { Matrix3D } from "../math/matrix-3d"; 6 | 7 | /** Class representing Radial-Intrack-Crosstrack (RIC) relative coordinates. */ 8 | export class RIC implements IStateVector { 9 | /** State UTC Epoch. */ 10 | public readonly epoch: EpochUTC; 11 | /** Relative position vector, in kilometers. */ 12 | public readonly position: Vector3D; 13 | /** Relative velocity vector, in kilometers per second. */ 14 | public readonly velocity: Vector3D; 15 | 16 | private reference: J2000 | null; 17 | private matrix: Matrix3D | null; 18 | 19 | /** 20 | * Create a new RIC object. 21 | * 22 | * @param epoch UTC epoch 23 | * @param position relative position, in kilometers 24 | * @param velocity relative velocity, in kilometers per second 25 | */ 26 | constructor(epoch: EpochUTC, position: Vector3D, velocity: Vector3D) { 27 | this.epoch = epoch; 28 | this.position = position; 29 | this.velocity = velocity; 30 | this.reference = null; 31 | this.matrix = null; 32 | } 33 | 34 | /** Return a string representation of this object. */ 35 | public toString(): string { 36 | const { epoch, position, velocity } = this; 37 | const output = [ 38 | "[RIC]", 39 | ` Epoch: ${epoch.toString()}`, 40 | ` Position: ${position.toString()} km`, 41 | ` Velocity: ${velocity.toString()} km/s` 42 | ]; 43 | return output.join("\n"); 44 | } 45 | 46 | /** 47 | * Create a RIC state for a J2000 state, relative to another J2000 state. 48 | * 49 | * This handles caching appropriate data for RIC to J2000 conversion. 50 | * 51 | * @param state J2000 state vector 52 | * @param reference target state for reference frame 53 | */ 54 | public static fromJ2kState(state: J2000, reference: J2000) { 55 | const ru = state.position.normalized(); 56 | const cu = state.position.cross(state.velocity).normalized(); 57 | const iu = cu.cross(ru); 58 | const matrix = new Matrix3D(ru, iu, cu); 59 | 60 | const dp = state.position.add(reference.position.negate()); 61 | const dv = state.velocity.add(reference.velocity.negate()); 62 | const ric = new RIC( 63 | state.epoch, 64 | matrix.multiplyVector3D(dp), 65 | matrix.multiplyVector3D(dv) 66 | ); 67 | ric.reference = reference; 68 | ric.matrix = matrix; 69 | return ric; 70 | } 71 | 72 | /** Convert this to a J2000 state vector object. */ 73 | public toJ2000() { 74 | this.matrix = this.matrix || Matrix3D.zeros(); 75 | this.reference = 76 | this.reference || 77 | new J2000(this.epoch, Vector3D.origin(), Vector3D.origin()); 78 | const ricMatrixT = this.matrix.transpose(); 79 | const dp = ricMatrixT.multiplyVector3D(this.position); 80 | const dv = ricMatrixT.multiplyVector3D(this.velocity); 81 | return new J2000( 82 | this.epoch, 83 | this.reference.position.add(dp), 84 | this.reference.velocity.add(dv) 85 | ); 86 | } 87 | 88 | /** 89 | * Create a new RIC object, with adjusted velocity. 90 | * 91 | * @param radial radial delta-V (km/s) 92 | * @param intrack intrack delta-V (km/s) 93 | * @param crosstrack crosstrack delta-V (km/s) 94 | */ 95 | public addVelocity(radial: number, intrack: number, crosstrack: number) { 96 | const deltaV = new Vector3D(radial, intrack, crosstrack); 97 | const ric = new RIC(this.epoch, this.position, this.velocity.add(deltaV)); 98 | ric.reference = this.reference; 99 | ric.matrix = this.matrix; 100 | return ric; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/math/matrix-3d.ts: -------------------------------------------------------------------------------- 1 | import { Vector3D } from "./vector-3d"; 2 | 3 | /** Internal Matrix3D storage format. */ 4 | export type Matrix3DValues = [Vector3D, Vector3D, Vector3D]; 5 | 6 | /** Class representing a 3x3 matrix. */ 7 | export class Matrix3D { 8 | /** matrix data */ 9 | private readonly matrix: Matrix3DValues; 10 | 11 | /** 12 | * Create a new Matrix3D object. 13 | * 14 | * @param a first row 15 | * @param b second row 16 | * @param c third row 17 | */ 18 | constructor(a: Vector3D, b: Vector3D, c: Vector3D) { 19 | this.matrix = [a, b, c]; 20 | } 21 | 22 | /** Create a new object, containing all zeros. */ 23 | public static zeros() { 24 | return new Matrix3D( 25 | Vector3D.origin(), 26 | Vector3D.origin(), 27 | Vector3D.origin() 28 | ); 29 | } 30 | 31 | /** Return a string representation of this matrix. */ 32 | public toString(): string { 33 | const a0Str = this.get(0, 0).toExponential(9); 34 | const a1Str = this.get(0, 1).toExponential(9); 35 | const a2Str = this.get(0, 2).toExponential(9); 36 | const b0Str = this.get(1, 0).toExponential(9); 37 | const b1Str = this.get(1, 1).toExponential(9); 38 | const b2Str = this.get(1, 2).toExponential(9); 39 | const c0Str = this.get(2, 0).toExponential(9); 40 | const c1Str = this.get(2, 1).toExponential(9); 41 | const c2Str = this.get(2, 2).toExponential(9); 42 | return [ 43 | `[ ${a0Str}, ${a1Str}, ${a2Str} ]`, 44 | `[ ${b0Str}, ${b1Str}, ${b2Str} ]`, 45 | `[ ${c0Str}, ${c1Str}, ${c2Str} ]` 46 | ].join("\n"); 47 | } 48 | 49 | /** 50 | * Get matrix data by index. 51 | * 52 | * @param row row index (0-2) 53 | * @param column column index (0-2) 54 | */ 55 | public get(row: number, column: number) { 56 | const rowVal = this.matrix[row]; 57 | if (column === 0) { 58 | return rowVal.x; 59 | } else if (column === 1) { 60 | return rowVal.y; 61 | } else if (column === 2) { 62 | return rowVal.z; 63 | } 64 | return 0; 65 | } 66 | 67 | /** 68 | * Linearly scale all matrix values by a number. 69 | * 70 | * @param n scalar 71 | */ 72 | public scale(n: number) { 73 | return new Matrix3D( 74 | this.matrix[0].scale(n), 75 | this.matrix[1].scale(n), 76 | this.matrix[2].scale(n) 77 | ); 78 | } 79 | 80 | /** Calculate and return the transpose of this matrix. */ 81 | public transpose() { 82 | const a = new Vector3D(this.get(0, 0), this.get(1, 0), this.get(2, 0)); 83 | const b = new Vector3D(this.get(0, 1), this.get(1, 1), this.get(2, 1)); 84 | const c = new Vector3D(this.get(0, 2), this.get(1, 2), this.get(2, 2)); 85 | return new Matrix3D(a, b, c); 86 | } 87 | 88 | /** 89 | * Multiply this by the vector argument. 90 | * 91 | * @param v 3-vector 92 | */ 93 | public multiplyVector3D(v: Vector3D) { 94 | const { matrix } = this; 95 | return new Vector3D(matrix[0].dot(v), matrix[1].dot(v), matrix[2].dot(v)); 96 | } 97 | 98 | /** Return the Cholesky decomposition of this matrix. */ 99 | public cholesky() { 100 | const a = this; 101 | const l = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; 102 | for (let i = 0; i < 3; i++) { 103 | for (let k = 0; k < i + 1; k++) { 104 | let sum = 0; 105 | for (let j = 0; j < k; j++) { 106 | sum += l[i][j] * l[k][j]; 107 | } 108 | l[i][k] = 109 | i === k 110 | ? Math.sqrt(a.get(i, i) - sum) 111 | : (1 / l[k][k]) * (a.get(i, k) - sum); 112 | } 113 | } 114 | return new Matrix3D( 115 | new Vector3D(l[0][0], l[0][1], l[0][2]), 116 | new Vector3D(l[1][0], l[1][1], l[1][2]), 117 | new Vector3D(l[2][0], l[2][1], l[2][2]) 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/examples/basic-example.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EpochUTC, 3 | Geodetic, 4 | J2000, 5 | RungeKutta4Propagator, 6 | Vector3D 7 | } from "../index"; 8 | 9 | // ============================================================================= 10 | // set initial state in J2000 frame 11 | // ============================================================================= 12 | 13 | const initialState = new J2000( 14 | EpochUTC.fromDateString("2018-12-21T00:00:00.000Z"), // epoch (UTC) 15 | new Vector3D(-1117.913276, 73.093299, -7000.018272), // km 16 | new Vector3D(3.531365461, 6.583914964, -0.495649656) // km/s 17 | ); 18 | 19 | console.log(initialState.toString()); 20 | // => [J2000] 21 | // Epoch: 2018-12-21T00:00:00.000Z 22 | // Position: [ -1117.913276000, 73.093299000, -7000.018272000 ] km 23 | // Velocity: [ 3.531365461, 6.583914964, -0.495649656 ] km/s 24 | 25 | // ============================================================================= 26 | // create a propagator object 27 | // ============================================================================= 28 | 29 | const propagator = new RungeKutta4Propagator(initialState); 30 | 31 | // set the step size 32 | propagator.setStepSize(5); // seconds 33 | 34 | // add Earth gravity acceleration 35 | propagator.forceModel.setEarthGravity( 36 | 50, // degree 37 | 50 // order 38 | ); 39 | 40 | // add third-body acceleration 41 | propagator.forceModel.setThirdBody( 42 | true, // moon gravity 43 | true // sun gravity 44 | ); 45 | 46 | // ============================================================================= 47 | // propagate ephemeris to a future time 48 | // ============================================================================= 49 | 50 | const finalState = propagator.propagate( 51 | EpochUTC.fromDateString("2018-12-22T00:00:00.000Z") 52 | ); 53 | 54 | console.log(finalState.toString()); 55 | // => [J2000] 56 | // Epoch: 2018-12-22T00:00:00.000Z 57 | // Position: [ -212.111629987, -2464.336270508, 6625.907441304 ] km 58 | // Velocity: [ -3.618621245, -6.126790740, -2.389539402 ] km/s 59 | 60 | // ============================================================================= 61 | // display information about the propagated state 62 | // ============================================================================= 63 | 64 | // Earth-fixed coordinates 65 | console.log(finalState.toITRF().toString()); 66 | // => [ITRF] 67 | // Epoch: 2018-12-22T00:00:00.000Z 68 | // Position: [ -2463.105532067, 235.348124556, 6625.580458844 ] km 69 | // Velocity: [ -6.093169860, 3.821763334, -2.395927109 ] km/s 70 | 71 | // geodetic coordinates 72 | console.log( 73 | finalState 74 | .toITRF() 75 | .toGeodetic() 76 | .toString() 77 | ); 78 | // => [Geodetic] 79 | // Latitude: 69.635° 80 | // Longitude: 174.542° 81 | // Altitude: 713.165 km 82 | 83 | // look angle from ground observer 84 | const observer = new Geodetic( 85 | 71.218 * (Math.PI / 180), // latitude (radians) 86 | 180.508 * (Math.PI / 180), // longitude (radians) 87 | 0.325 // altitude (km) 88 | ); 89 | console.log( 90 | finalState 91 | .toITRF() 92 | .toLookAngle(observer) 93 | .toString() 94 | ); 95 | // => [Look-Angle] 96 | // Azimuth: 234.477° 97 | // Elevation: 65.882° 98 | // Range: 773.318 km 99 | 100 | // relative position 101 | const actualState = new J2000( 102 | EpochUTC.fromDateString("2018-12-22T00:00:00.000Z"), 103 | new Vector3D(-212.125533, -2464.351601, 6625.907454), 104 | new Vector3D(-3.618617698, -6.12677853, -2.38955619) 105 | ); 106 | console.log(finalState.toRIC(actualState).toString()); 107 | // => [RIC] 108 | // Epoch: 2018-12-22T00:00:00.000Z 109 | // Position: [ -0.005770585, -0.019208198, 0.005105235 ] km 110 | // Velocity: [ 0.000020089, 0.000006319, 0.000000117 ] km/s 111 | -------------------------------------------------------------------------------- /src/propagators/interpolator-propagator.ts: -------------------------------------------------------------------------------- 1 | import { J2000 } from "../coordinates/j2000"; 2 | import { Vector3D } from "../math/vector-3d"; 3 | import { EpochUTC } from "../time/epoch-utc"; 4 | import { IPropagator } from "./propagator-interface"; 5 | import { RungeKutta4Propagator } from "./runge-kutta-4-propagator"; 6 | 7 | /** Interpolator for cached satellite ephemeris. */ 8 | export class InterpolatorPropagator implements IPropagator { 9 | /** Internal propagator object. */ 10 | private readonly propagator: RungeKutta4Propagator; 11 | /** Cache of J2000 states. */ 12 | private readonly initStates: J2000[]; 13 | 14 | /** 15 | * Create a new Interpolator Propagator object. 16 | * 17 | * @param states list of J2000 state vectors 18 | */ 19 | constructor(states: J2000[]) { 20 | this.initStates = states; 21 | this.sortStates(); 22 | this.propagator = new RungeKutta4Propagator(this.initStates[0]); 23 | this.setStepSize(60); 24 | } 25 | 26 | /** Chronologically sort cached states. */ 27 | private sortStates() { 28 | this.initStates.sort((a, b) => a.epoch.unix - b.epoch.unix); 29 | } 30 | 31 | /** 32 | * Set propagator step size. 33 | * 34 | * @param seconds step size, in seconds 35 | */ 36 | public setStepSize(seconds: number) { 37 | this.propagator.setStepSize(seconds); 38 | } 39 | 40 | /** Fetch last propagated satellite state. */ 41 | get state() { 42 | return this.propagator.state; 43 | } 44 | 45 | /** Reset cached state to the initialized state. */ 46 | public reset() { 47 | this.propagator.setInitState(this.initStates[0]); 48 | } 49 | 50 | /** Propagator force model object. */ 51 | get forceModel() { 52 | return this.propagator.forceModel; 53 | } 54 | 55 | /** 56 | * Return the cached state closest to the proved epoch. 57 | * 58 | * @param epoch UTC epoch 59 | */ 60 | private getNearest(epoch: EpochUTC) { 61 | if (this.initStates.length === 0) { 62 | return new J2000(epoch, Vector3D.origin(), Vector3D.origin()); 63 | } 64 | const unix = epoch.unix; 65 | if (unix < this.initStates[0].epoch.unix) { 66 | return this.initStates[0]; 67 | } 68 | if (unix > this.initStates[this.initStates.length - 1].epoch.unix) { 69 | return this.initStates[this.initStates.length - 1]; 70 | } 71 | let low = 0; 72 | let high = this.initStates.length - 1; 73 | while (low <= high) { 74 | const mid = Math.floor((high + low) / 2); 75 | const midVal = this.initStates[mid].epoch.unix; 76 | if (unix < midVal) { 77 | high = mid - 1; 78 | } else if (unix > midVal) { 79 | low = mid + 1; 80 | } else { 81 | return this.initStates[mid]; 82 | } 83 | } 84 | const lowDiff = this.initStates[low].epoch.unix - unix; 85 | const highDiff = unix - this.initStates[high].epoch.unix; 86 | return lowDiff < highDiff ? this.initStates[low] : this.initStates[high]; 87 | } 88 | 89 | /** 90 | * Interpolate cached states to a new epoch. 91 | * 92 | * @param newEpoch propagation epoch 93 | */ 94 | public propagate(newEpoch: EpochUTC) { 95 | this.propagator.setInitState(this.getNearest(newEpoch)); 96 | this.propagator.propagate(newEpoch); 97 | return this.state; 98 | } 99 | 100 | /** 101 | * Interpolate state by some number of seconds, repeatedly, starting at a 102 | * specified epoch. 103 | * 104 | * @param epoch UTC epoch 105 | * @param interval seconds between output states 106 | * @param count number of steps to take 107 | */ 108 | public step(epoch: EpochUTC, interval: number, count: number) { 109 | const output: J2000[] = [this.propagate(epoch)]; 110 | let tempEpoch = epoch; 111 | for (let i = 0; i < count; i++) { 112 | tempEpoch = tempEpoch.roll(interval); 113 | output.push(this.propagate(tempEpoch)); 114 | } 115 | return output; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/data/values/iau1980.ts: -------------------------------------------------------------------------------- 1 | /**a1, a2, a3, a4, a5, Ai, Bi, Ci, Di */ 2 | export type Iau1980Data = [ 3 | number, 4 | number, 5 | number, 6 | number, 7 | number, 8 | number, 9 | number, 10 | number, 11 | number 12 | ]; 13 | 14 | /** IAU1980 coefficients. */ 15 | export const IAU_1980: Iau1980Data[] = [ 16 | [0, 0, 0, 0, 1, -171996, -174.2, 92025, 8.9], 17 | [0, 0, 2, -2, 2, -13187, -1.6, 5736, -3.1], 18 | [0, 0, 2, 0, 2, -2274, -0.2, 977, -0.5], 19 | [0, 0, 0, 0, 2, 2062, 0.2, -895, 0.5], 20 | [0, 1, 0, 0, 0, 1426, -3.4, 54, -0.1], 21 | [1, 0, 0, 0, 0, 712, 0.1, -7, 0], 22 | [0, 1, 2, -2, 2, -517, 1.2, 224, -0.6], 23 | [0, 0, 2, 0, 1, -386, -0.4, 200, 0], 24 | [1, 0, 2, 0, 2, -301, 0, 129, -0.1], 25 | [0, -1, 2, -2, 2, 217, -0.5, -95, 0.3], 26 | [1, 0, 0, -2, 0, -158, 0, -1, 0], 27 | [0, 0, 2, -2, 1, 129, 0.1, -70, 0], 28 | [-1, 0, 2, 0, 2, 123, 0, -53, 0], 29 | [1, 0, 0, 0, 1, 63, 0.1, -33, 0], 30 | [0, 0, 0, 2, 0, 63, 0, -2, 0], 31 | [-1, 0, 2, 2, 2, -59, 0, 26, 0], 32 | [-1, 0, 0, 0, 1, -58, -0.1, 32, 0], 33 | [1, 0, 2, 0, 1, -51, 0, 27, 0], 34 | [2, 0, 0, -2, 0, 48, 0, 1, 0], 35 | [-2, 0, 2, 0, 1, 46, 0, -24, 0], 36 | [0, 0, 2, 2, 2, -38, 0, 16, 0], 37 | [2, 0, 2, 0, 2, -31, 0, 13, 0], 38 | [2, 0, 0, 0, 0, 29, 0, -1, 0], 39 | [1, 0, 2, -2, 2, 29, 0, -12, 0], 40 | [0, 0, 2, 0, 0, 26, 0, -1, 0], 41 | [0, 0, 2, -2, 0, -22, 0, 0, 0], 42 | [-1, 0, 2, 0, 1, 21, 0, -10, 0], 43 | [0, 2, 0, 0, 0, 17, -0.1, 0, 0], 44 | [0, 2, 2, -2, 2, -16, 0.1, 7, 0], 45 | [-1, 0, 0, 2, 1, 16, 0, -8, 0], 46 | [0, 1, 0, 0, 1, -15, 0, 9, 0], 47 | [1, 0, 0, -2, 1, -13, 0, 7, 0], 48 | [0, -1, 0, 0, 1, -12, 0, 6, 0], 49 | [2, 0, -2, 0, 0, 11, 0, 0, 0], 50 | [-1, 0, 2, 2, 1, -10, 0, 5, 0], 51 | [1, 0, 2, 2, 2, -8, 0, 3, 0], 52 | [0, -1, 2, 0, 2, -7, 0, 3, 0], 53 | [0, 0, 2, 2, 1, -7, 0, 3, 0], 54 | [1, 1, 0, -2, 0, -7, 0, 0, 0], 55 | [0, 1, 2, 0, 2, 7, 0, -3, 0], 56 | [-2, 0, 0, 2, 1, -6, 0, 3, 0], 57 | [0, 0, 0, 2, 1, -6, 0, 3, 0], 58 | [2, 0, 2, -2, 2, 6, 0, -3, 0], 59 | [1, 0, 0, 2, 0, 6, 0, 0, 0], 60 | [1, 0, 2, -2, 1, 6, 0, -3, 0], 61 | [0, 0, 0, -2, 1, -5, 0, 3, 0], 62 | [0, -1, 2, -2, 1, -5, 0, 3, 0], 63 | [2, 0, 2, 0, 1, -5, 0, 3, 0], 64 | [1, -1, 0, 0, 0, 5, 0, 0, 0], 65 | [1, 0, 0, -1, 0, -4, 0, 0, 0], 66 | [0, 0, 0, 1, 0, -4, 0, 0, 0], 67 | [0, 1, 0, -2, 0, -4, 0, 0, 0], 68 | [1, 0, -2, 0, 0, 4, 0, 0, 0], 69 | [2, 0, 0, -2, 1, 4, 0, -2, 0], 70 | [0, 1, 2, -2, 1, 4, 0, -2, 0], 71 | [1, 1, 0, 0, 0, -3, 0, 0, 0], 72 | [1, -1, 0, -1, 0, -3, 0, 0, 0], 73 | [-1, -1, 2, 2, 2, -3, 0, 1, 0], 74 | [0, -1, 2, 2, 2, -3, 0, 1, 0], 75 | [1, -1, 2, 0, 2, -3, 0, 1, 0], 76 | [3, 0, 2, 0, 2, -3, 0, 1, 0], 77 | [-2, 0, 2, 0, 2, -3, 0, 1, 0], 78 | [1, 0, 2, 0, 0, 3, 0, 0, 0], 79 | [-1, 0, 2, 4, 2, -2, 0, 1, 0], 80 | [1, 0, 0, 0, 2, -2, 0, 1, 0], 81 | [-1, 0, 2, -2, 1, -2, 0, 1, 0], 82 | [0, -2, 2, -2, 1, -2, 0, 1, 0], 83 | [-2, 0, 0, 0, 1, -2, 0, 1, 0], 84 | [2, 0, 0, 0, 1, 2, 0, -1, 0], 85 | [3, 0, 0, 0, 0, 2, 0, 0, 0], 86 | [1, 1, 2, 0, 2, 2, 0, -1, 0], 87 | [0, 0, 2, 1, 2, 2, 0, -1, 0], 88 | [1, 0, 0, 2, 1, -1, 0, 0, 0], 89 | [1, 0, 2, 2, 1, -1, 0, 1, 0], 90 | [1, 1, 0, -2, 1, -1, 0, 0, 0], 91 | [0, 1, 0, 2, 0, -1, 0, 0, 0], 92 | [0, 1, 2, -2, 0, -1, 0, 0, 0], 93 | [0, 1, -2, 2, 0, -1, 0, 0, 0], 94 | [1, 0, -2, 2, 0, -1, 0, 0, 0], 95 | [1, 0, -2, -2, 0, -1, 0, 0, 0], 96 | [1, 0, 2, -2, 0, -1, 0, 0, 0], 97 | [1, 0, 0, -4, 0, -1, 0, 0, 0], 98 | [2, 0, 0, -4, 0, -1, 0, 0, 0], 99 | [0, 0, 2, 4, 2, -1, 0, 0, 0], 100 | [0, 0, 2, -1, 2, -1, 0, 0, 0], 101 | [-2, 0, 2, 4, 2, -1, 0, 1, 0], 102 | [2, 0, 2, 2, 2, -1, 0, 0, 0], 103 | [0, -1, 2, 0, 1, -1, 0, 0, 0], 104 | [0, 0, -2, 0, 1, -1, 0, 0, 0], 105 | [0, 0, 4, -2, 2, 1, 0, 0, 0], 106 | [0, 1, 0, 0, 2, 1, 0, 0, 0], 107 | [1, 1, 2, -2, 2, 1, 0, -1, 0], 108 | [3, 0, 2, -2, 2, 1, 0, 0, 0], 109 | [-2, 0, 2, 2, 2, 1, 0, -1, 0], 110 | [-1, 0, 0, 0, 2, 1, 0, -1, 0], 111 | [0, 0, -2, 2, 1, 1, 0, 0, 0], 112 | [0, 1, 2, 0, 1, 1, 0, 0, 0], 113 | [-1, 0, 4, 0, 2, 1, 0, 0, 0], 114 | [2, 1, 0, -2, 0, 1, 0, 0, 0], 115 | [2, 0, 0, 2, 0, 1, 0, 0, 0], 116 | [2, 0, 2, -2, 1, 1, 0, -1, 0], 117 | [2, 0, -2, 0, 1, 1, 0, 0, 0], 118 | [1, -1, 0, -2, 0, 1, 0, 0, 0], 119 | [-1, 0, 0, 1, 1, 1, 0, 0, 0], 120 | [-1, -1, 0, 2, 1, 1, 0, 0, 0], 121 | [0, 1, 0, 1, 0, 1, 0, 0, 0] 122 | ]; 123 | -------------------------------------------------------------------------------- /src/bodies/earth-body.ts: -------------------------------------------------------------------------------- 1 | import { DataHandler } from "../data/data-handler"; 2 | import { IAU_1980 } from "../data/values/iau1980"; 3 | import { DEG2RAD, TTASEC2RAD } from "../math/constants"; 4 | import { evalPoly } from "../math/operations"; 5 | import { Vector3D } from "../math/vector-3d"; 6 | import { EpochUTC } from "../time/epoch-utc"; 7 | 8 | export class EarthBody { 9 | /** Earth gravitational parameter, in km^3/s^2. */ 10 | public static readonly MU = 398600.4415; 11 | 12 | /** Earth equatorial radius, in kilometers. */ 13 | public static readonly RADIUS_EQUATOR = 6378.1363; 14 | 15 | /** Earth coefficient of flattening. */ 16 | public static readonly FLATTENING = 1 / 298.257; 17 | 18 | /** Earth rotation vector, in radians per second. */ 19 | private static readonly ROTATION = new Vector3D(0, 0, 7.2921158553e-5); 20 | 21 | /** Earth polar radius, in kilometers. */ 22 | public static readonly RADIUS_POLAR = 23 | EarthBody.RADIUS_EQUATOR * (1 - EarthBody.FLATTENING); 24 | 25 | /** Earth mean radius, in kilometers. */ 26 | public static readonly RADIUS_MEAN = 27 | (2 * EarthBody.RADIUS_EQUATOR + EarthBody.RADIUS_POLAR) / 3; 28 | 29 | /** Earth eccentricity squared. */ 30 | public static readonly ECCENTRICITY_SQUARED = 31 | EarthBody.FLATTENING * (2 - EarthBody.FLATTENING); 32 | 33 | /** 34 | * Return Earth's rotation vector, in radians per second. 35 | * 36 | * The vector is adjusted for length of day (LOD) variations, assuming IERS 37 | * finals.all data is loaded in DataHandler. Otherwise, LOD corrections are 38 | * ignored. 39 | * 40 | * @param epoch UTC epoch 41 | */ 42 | public static getRotation(epoch: EpochUTC) { 43 | const finals = DataHandler.getFinalsData(epoch.toMjd()); 44 | return EarthBody.ROTATION.scale(1 - finals.lod / 86400); 45 | } 46 | 47 | /** 48 | * Calculate the [zeta, theta, zed] angles of precession, in radians. 49 | * 50 | * @param epoch satellite state epoch 51 | */ 52 | public static precession(epoch: EpochUTC): [number, number, number] { 53 | const t = epoch.toTDB().toJulianCenturies(); 54 | const zeta = evalPoly(t, [0.0, 0.6406161, 0.0000839, 5.0e-6]); 55 | const theta = evalPoly(t, [0.0, 0.556753, -0.0001185, -1.16e-5]); 56 | const zed = evalPoly(t, [0.0, 0.6406161, 0.0003041, 5.1e-6]); 57 | return [zeta * DEG2RAD, theta * DEG2RAD, zed * DEG2RAD]; 58 | } 59 | 60 | /** 61 | * Calculate the [deltaPsi, deltaEpsilon, meanEpsilon] angles of nutation, 62 | * in radians. 63 | * 64 | * @param epoch satellite state epoch 65 | * @param n number of coefficients (default=106) 66 | */ 67 | public static nutation(epoch: EpochUTC, n = 106): [number, number, number] { 68 | const r = 360; 69 | const t = epoch.toTDB().toJulianCenturies(); 70 | const moonAnom = evalPoly(t, [ 71 | 134.96340251, 72 | 1325.0 * r + 198.8675605, 73 | 0.0088553, 74 | 1.4343e-5 75 | ]); 76 | const sunAnom = evalPoly(t, [ 77 | 357.52910918, 78 | 99.0 * r + 359.0502911, 79 | -0.0001537, 80 | 3.8e-8 81 | ]); 82 | const moonLat = evalPoly(t, [ 83 | 93.27209062, 84 | 1342.0 * r + 82.0174577, 85 | -0.003542, 86 | -2.88e-7 87 | ]); 88 | const sunElong = evalPoly(t, [ 89 | 297.85019547, 90 | 1236.0 * r + 307.1114469, 91 | -0.0017696, 92 | 1.831e-6 93 | ]); 94 | const moonRaan = evalPoly(t, [ 95 | 125.04455501, 96 | -(5.0 * r + 134.1361851), 97 | 0.0020756, 98 | 2.139e-6 99 | ]); 100 | let deltaPsi = 0; 101 | let deltaEpsilon = 0; 102 | IAU_1980.slice(0, n).map(iauLine => { 103 | let [a1, a2, a3, a4, a5, Ai, Bi, Ci, Di] = iauLine; 104 | const arg = 105 | (a1 * moonAnom + 106 | a2 * sunAnom + 107 | a3 * moonLat + 108 | a4 * sunElong + 109 | a5 * moonRaan) * 110 | DEG2RAD; 111 | const sinC = Ai + Bi * t; 112 | const cosC = Ci + Di * t; 113 | deltaPsi += sinC * Math.sin(arg); 114 | deltaEpsilon += cosC * Math.cos(arg); 115 | }); 116 | deltaPsi = deltaPsi * TTASEC2RAD; 117 | deltaEpsilon = deltaEpsilon * TTASEC2RAD; 118 | const meanEpsilon = 119 | evalPoly(t, [23.439291, -0.013004, -1.64e-7, 5.04e-7]) * DEG2RAD; 120 | return [deltaPsi, deltaEpsilon, meanEpsilon]; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/forces/force-model.ts: -------------------------------------------------------------------------------- 1 | import { J2000 } from "../coordinates/j2000"; 2 | import { Vector3D } from "../math/vector-3d"; 3 | import { Vector6D } from "../math/vector-6d"; 4 | import { AtmosphericDrag } from "./atmospheric-drag"; 5 | import { EarthGravity } from "./earth-gravity"; 6 | import { AccelerationMap } from "./forces-interface"; 7 | import { SolarRadiationPressure } from "./solar-radiation-pressure"; 8 | import { ThirdBody } from "./third-body"; 9 | 10 | /** Object for efficiently managing acceleration forces on a spacecraft. */ 11 | export class ForceModel { 12 | /** Earth gravity model, if applicable */ 13 | private earthGravity: EarthGravity | null; 14 | /** third-body model, if applicable */ 15 | private thirdBody: ThirdBody | null; 16 | /** atmospheric drag model, if applicable */ 17 | private atmosphericDrag: AtmosphericDrag | null; 18 | /** solar radiation pressure model, if applicable */ 19 | private solarRadiationPressure: SolarRadiationPressure | null; 20 | 21 | /** Create a new ForceModel object. */ 22 | constructor() { 23 | this.earthGravity = null; 24 | this.thirdBody = null; 25 | this.atmosphericDrag = null; 26 | this.solarRadiationPressure = null; 27 | } 28 | 29 | /** Clear all current AccelerationForce models for this object. */ 30 | public clearModel() { 31 | this.earthGravity = null; 32 | this.thirdBody = null; 33 | this.atmosphericDrag = null; 34 | this.solarRadiationPressure = null; 35 | } 36 | 37 | /** 38 | * Create and add a new EarthGravity force to this object. 39 | * 40 | * @param degree geopotential degree (max=70) 41 | * @param order geopotential order (max=70) 42 | */ 43 | public setEarthGravity(degree: number, order: number) { 44 | this.earthGravity = new EarthGravity(degree, order); 45 | } 46 | 47 | /** 48 | * Create and add a new ThirdBody force to this object. 49 | * 50 | * @param moon moon gravity, if true 51 | * @param sun sun gravity, if true 52 | */ 53 | public setThirdBody(moon: boolean, sun: boolean) { 54 | this.thirdBody = new ThirdBody(moon, sun); 55 | } 56 | 57 | /** 58 | * Create and add a new AtmosphericDrag force to this object. 59 | * 60 | * @param mass spacecraft mass, in kilograms 61 | * @param area spacecraft area, in square meters 62 | * @param dragCoeff drag coefficient (default=2.2) 63 | */ 64 | public setAtmosphericDrag(mass: number, area: number, dragCoeff = 2.2) { 65 | this.atmosphericDrag = new AtmosphericDrag(mass, area, dragCoeff); 66 | } 67 | 68 | /** 69 | * Create and add a new SolarRadiationPressure force to this object. 70 | * 71 | * @param mass spacecraft mass, in kilograms 72 | * @param area spacecraft area, in square meters 73 | * @param reflectCoeff reflectivity coefficient (default=1.2) 74 | */ 75 | public setSolarRadiationPressure( 76 | mass: number, 77 | area: number, 78 | reflectCoeff = 1.2 79 | ) { 80 | this.solarRadiationPressure = new SolarRadiationPressure( 81 | mass, 82 | area, 83 | reflectCoeff 84 | ); 85 | } 86 | 87 | /** 88 | * Create an acceleration map argument with calculated values for each 89 | * acceleration source, for the provided state vector. 90 | * 91 | * @param j2kState J2000 state vector 92 | */ 93 | public accelerations(j2kState: J2000) { 94 | const accMap: AccelerationMap = {}; 95 | if (this.earthGravity !== null) { 96 | this.earthGravity.acceleration(j2kState, accMap); 97 | } 98 | if (this.thirdBody !== null) { 99 | this.thirdBody.acceleration(j2kState, accMap); 100 | } 101 | if (this.atmosphericDrag !== null) { 102 | this.atmosphericDrag.acceleration(j2kState, accMap); 103 | } 104 | if (this.solarRadiationPressure !== null) { 105 | this.solarRadiationPressure.acceleration(j2kState, accMap); 106 | } 107 | return accMap; 108 | } 109 | 110 | /** 111 | * Calculate and return the 6-dimensional derivative (velocity, acceleration) 112 | * for the provided state vector. 113 | * 114 | * @param j2kState J2000 state vector 115 | */ 116 | public derivative(j2kState: J2000) { 117 | const accMap = this.accelerations(j2kState); 118 | let accel = Vector3D.origin(); 119 | for (let k in accMap) { 120 | accel = accel.add(accMap[k]); 121 | } 122 | const { x: vx, y: vy, z: vz } = j2kState.velocity; 123 | const { x: ax, y: ay, z: az } = accel; 124 | return new Vector6D(vx, vy, vz, ax, ay, az); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/propagators/runge-kutta-4-propagator.ts: -------------------------------------------------------------------------------- 1 | import { J2000 } from "../coordinates/j2000"; 2 | import { ForceModel } from "../forces/force-model"; 3 | import { copySign } from "../math/operations"; 4 | import { Vector6D } from "../math/vector-6d"; 5 | import { EpochUTC } from "../time/epoch-utc"; 6 | import { IPropagator } from "./propagator-interface"; 7 | 8 | /** 4th order Runge-Kutta numerical integrator for satellite propagation. */ 9 | export class RungeKutta4Propagator implements IPropagator { 10 | /** force model */ 11 | public readonly forceModel: ForceModel; 12 | /** initial state */ 13 | private initState: J2000; 14 | /** cached state */ 15 | private cacheState: J2000; 16 | /** step size (seconds) */ 17 | private stepSize: number; 18 | 19 | /** 20 | * Create a new RungeKutta4 propagator object. 21 | * 22 | * @param state J2000 state vector 23 | */ 24 | constructor(state: J2000) { 25 | this.initState = state; 26 | this.cacheState = this.initState; 27 | this.stepSize = 15; 28 | this.forceModel = new ForceModel(); 29 | this.forceModel.setEarthGravity(0, 0); 30 | } 31 | 32 | /** Fetch last propagated satellite state. */ 33 | get state() { 34 | return this.cacheState; 35 | } 36 | 37 | /** 38 | * Set the integration step size. 39 | * 40 | * Smaller is slower, but more accurate. 41 | * 42 | * @param seconds step size (seconds) 43 | */ 44 | public setStepSize(seconds: number) { 45 | this.stepSize = Math.abs(seconds); 46 | } 47 | 48 | /** 49 | * Set the propagator initial state. 50 | * 51 | * @param state J2000 state 52 | */ 53 | public setInitState(state: J2000) { 54 | this.initState = state; 55 | this.reset(); 56 | } 57 | 58 | /** Reset cached state to the initialized state. */ 59 | public reset() { 60 | this.cacheState = this.initState; 61 | } 62 | 63 | /** 64 | * Calculate partial derivatives for integrations. 65 | * 66 | * @param j2kState J2000 state vector 67 | * @param hArg step size argument 68 | * @param kArg derivative argument 69 | */ 70 | private kFn(j2kState: J2000, hArg: number, kArg: Vector6D) { 71 | const epoch = j2kState.epoch.roll(hArg); 72 | const posvel = j2kState.position.join(j2kState.velocity); 73 | const [position, velocity] = posvel.add(kArg).split(); 74 | const sample = new J2000(epoch, position, velocity); 75 | return this.forceModel.derivative(sample); 76 | } 77 | 78 | /** 79 | * Calculate a future state by integrating velocity and acceleration. 80 | * 81 | * @param j2kState J2000 state vector 82 | * @param step step size (seconds) 83 | */ 84 | private integrate(j2kState: J2000, step: number) { 85 | const k1 = this.kFn(j2kState, 0, Vector6D.origin()).scale(step); 86 | const k2 = this.kFn(j2kState, step / 2, k1.scale(1 / 2)).scale(step); 87 | const k3 = this.kFn(j2kState, step / 2, k2.scale(1 / 2)).scale(step); 88 | const k4 = this.kFn(j2kState, step, k3).scale(step); 89 | const v1 = k1; 90 | const v2 = v1.add(k2.scale(2)); 91 | const v3 = v2.add(k3.scale(2)); 92 | const v4 = v3.add(k4); 93 | const tNext = j2kState.epoch.roll(step); 94 | const posvel = j2kState.position.join(j2kState.velocity); 95 | const [position, velocity] = posvel.add(v4.scale(1 / 6)).split(); 96 | return new J2000(tNext, position, velocity); 97 | } 98 | 99 | /** 100 | * Integrate cached state to a new epoch. 101 | * 102 | * @param newEpoch propagation epoch 103 | */ 104 | public propagate(newEpoch: EpochUTC) { 105 | while (!this.cacheState.epoch.equals(newEpoch)) { 106 | const delta = newEpoch.difference(this.cacheState.epoch); 107 | const mag = Math.min(this.stepSize, Math.abs(delta)); 108 | const step = copySign(mag, delta); 109 | this.cacheState = this.integrate(this.cacheState, step); 110 | } 111 | return this.cacheState; 112 | } 113 | 114 | /** 115 | * Propagate state by some number of seconds, repeatedly, starting at a 116 | * specified epoch. 117 | * 118 | * @param epoch UTC epoch 119 | * @param interval seconds between output states 120 | * @param count number of steps to take 121 | */ 122 | public step(epoch: EpochUTC, interval: number, count: number): J2000[] { 123 | const output: J2000[] = [this.propagate(epoch)]; 124 | let tempEpoch = epoch; 125 | for (let i = 0; i < count; i++) { 126 | tempEpoch = tempEpoch.roll(interval); 127 | output.push(this.propagate(tempEpoch)); 128 | } 129 | return output; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/coordinates/itrf.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { DataHandler } from "../data/data-handler"; 3 | import { Vector3D } from "../math/vector-3d"; 4 | import { EpochUTC } from "../time/epoch-utc"; 5 | import { IStateVector } from "./coordinate-interface"; 6 | import { Geodetic } from "./geodetic"; 7 | import { J2000 } from "./j2000"; 8 | import { LookAngle } from "./look-angle"; 9 | 10 | /** Class representing ITRF Earth-Fixed coordinates. */ 11 | export class ITRF implements IStateVector { 12 | /** Satellite state epoch. */ 13 | public readonly epoch: EpochUTC; 14 | /** Position vector, in kilometers. */ 15 | public readonly position: Vector3D; 16 | /** Velocity vector, in kilometers per second. */ 17 | public readonly velocity: Vector3D; 18 | 19 | /** 20 | * Create a new ITRF object. 21 | * 22 | * @param epoch UTC epoch 23 | * @param position ITRF position, in kilometers 24 | * @param velocity ITRF velocity, in kilometers per second 25 | */ 26 | constructor(epoch: EpochUTC, position: Vector3D, velocity?: Vector3D) { 27 | this.epoch = epoch; 28 | this.position = position; 29 | this.velocity = velocity || Vector3D.origin(); 30 | } 31 | 32 | /** Return a string representation of this object. */ 33 | public toString(): string { 34 | const { epoch, position, velocity } = this; 35 | const output = [ 36 | "[ITRF]", 37 | ` Epoch: ${epoch.toString()}`, 38 | ` Position: ${position.toString()} km`, 39 | ` Velocity: ${velocity.toString()} km/s` 40 | ]; 41 | return output.join("\n"); 42 | } 43 | 44 | /** Convert this to a J2000 state vector object. */ 45 | public toJ2000() { 46 | const { epoch, position, velocity } = this; 47 | const finals = DataHandler.getFinalsData(epoch.toMjd()); 48 | const pmX = finals.pmX; 49 | const pmY = finals.pmY; 50 | const rPEF = position.rot2(pmX).rot1(pmY); 51 | const vPEF = velocity.rot2(pmX).rot1(pmY); 52 | const [dPsi, dEps, mEps] = EarthBody.nutation(epoch); 53 | const epsilon = mEps + dEps; 54 | const ast = epoch.gmstAngle() + dPsi * Math.cos(epsilon); 55 | const rTOD = rPEF.rot3(-ast); 56 | const vTOD = vPEF 57 | .add(EarthBody.getRotation(this.epoch).cross(rPEF)) 58 | .rot3(-ast); 59 | const rMOD = rTOD 60 | .rot1(epsilon) 61 | .rot3(dPsi) 62 | .rot1(-mEps); 63 | const vMOD = vTOD 64 | .rot1(epsilon) 65 | .rot3(dPsi) 66 | .rot1(-mEps); 67 | const [zeta, theta, zed] = EarthBody.precession(epoch); 68 | const rJ2000 = rMOD 69 | .rot3(zed) 70 | .rot2(-theta) 71 | .rot3(zeta); 72 | const vJ2000 = vMOD 73 | .rot3(zed) 74 | .rot2(-theta) 75 | .rot3(zeta); 76 | return new J2000(epoch, rJ2000, vJ2000); 77 | } 78 | 79 | /** Convert to a Geodetic coordinate object. */ 80 | public toGeodetic() { 81 | const { 82 | position: { x, y, z } 83 | } = this; 84 | var sma = EarthBody.RADIUS_EQUATOR; 85 | var esq = EarthBody.ECCENTRICITY_SQUARED; 86 | var lon = Math.atan2(y, x); 87 | var r = Math.sqrt(x * x + y * y); 88 | var phi = Math.atan(z / r); 89 | var lat = phi; 90 | var c = 0.0; 91 | for (var i = 0; i < 6; i++) { 92 | var slat = Math.sin(lat); 93 | c = 1 / Math.sqrt(1 - esq * slat * slat); 94 | lat = Math.atan((z + sma * c * esq * Math.sin(lat)) / r); 95 | } 96 | var alt = r / Math.cos(lat) - sma * c; 97 | return new Geodetic(lat, lon, alt); 98 | } 99 | 100 | /** 101 | * Calculate the look angle between an observer and this. 102 | * 103 | * @param observer geodetic coordinates 104 | */ 105 | public toLookAngle(observer: Geodetic) { 106 | const { latitude, longitude } = observer; 107 | const { sin, cos } = Math; 108 | const { x: oX, y: oY, z: oZ } = observer.toITRF(this.epoch).position; 109 | const { x: tX, y: tY, z: tZ } = this.position; 110 | const [rX, rY, rZ] = [tX - oX, tY - oY, tZ - oZ]; 111 | const s = 112 | sin(latitude) * cos(longitude) * rX + 113 | sin(latitude) * sin(longitude) * rY - 114 | cos(latitude) * rZ; 115 | const e = -sin(longitude) * rX + cos(longitude) * rY; 116 | const z = 117 | cos(latitude) * cos(longitude) * rX + 118 | cos(latitude) * sin(longitude) * rY + 119 | sin(latitude) * rZ; 120 | const range = Math.sqrt(s * s + e * e + z * z); 121 | const elevation = Math.asin(z / range); 122 | const azimuth = Math.atan2(-e, s) + Math.PI; 123 | return new LookAngle(azimuth, elevation, range); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/examples/propagator-example.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EpochUTC, 3 | J2000, 4 | KeplerPropagator, 5 | RungeKutta4Propagator, 6 | Vector3D 7 | } from "../index"; 8 | 9 | //============================================================================== 10 | // define an initial state 11 | //============================================================================== 12 | 13 | // initial state in J2000 frame 14 | const initialState = new J2000( 15 | EpochUTC.fromDateString("2018-12-21T00:00:00.000Z"), // epoch (UTC) 16 | new Vector3D(-1117.913276, 73.093299, -7000.018272), // km 17 | new Vector3D(3.531365461, 6.583914964, -0.495649656) // km/s 18 | ); 19 | 20 | console.log(initialState.toString()); 21 | // => [J2000] 22 | // Epoch: 2018-12-21T00:00:00.000Z 23 | // Position: [ -1117.913276000, 73.093299000, -7000.018272000 ] km 24 | // Velocity: [ 3.531365461, 6.583914964, -0.495649656 ] km/s 25 | 26 | // real-world expected state (24-hours into the future) 27 | const expectedState = new J2000( 28 | EpochUTC.fromDateString("2018-12-22T00:00:00.000Z"), 29 | new Vector3D(-212.125533, -2464.351601, 6625.907454), 30 | new Vector3D(-3.618617698, -6.12677853, -2.38955619) 31 | ); 32 | 33 | console.log(expectedState.toString()); 34 | // => [J2000] 35 | // Epoch: 2018-12-22T00:00:00.000Z 36 | // Position: [ -212.125533000, -2464.351601000, 6625.907454000 ] km 37 | // Velocity: [ -3.618617698, -6.126778530, -2.389556190 ] km/s 38 | 39 | //============================================================================== 40 | // propagate state vector (numerical high-accuracy) 41 | //============================================================================== 42 | 43 | // create a propagator using J2000 state 44 | const hiAccProp = new RungeKutta4Propagator(initialState); 45 | 46 | // set step size 47 | hiAccProp.setStepSize(5); // seconds 48 | 49 | // add earth gravity 50 | hiAccProp.forceModel.setEarthGravity( 51 | 50, // degree 52 | 50 // order 53 | ); 54 | 55 | // add sun & moon gravity 56 | hiAccProp.forceModel.setThirdBody( 57 | true, // moon 58 | true // sun 59 | ); 60 | 61 | // add atmospheric drag 62 | hiAccProp.forceModel.setAtmosphericDrag( 63 | 2200, // mass (kg) 64 | 3.7, // area (m^2) 65 | 2.2 // drag coefficient 66 | ); 67 | 68 | // add solar radiation pressure 69 | hiAccProp.forceModel.setSolarRadiationPressure( 70 | 2200, // mass (kg) 71 | 3.7, // area (m^2) 72 | 1.2 // reflectivity coefficient 73 | ); 74 | 75 | // propagated state (24-hours into the future) 76 | const resultState = hiAccProp.propagate( 77 | EpochUTC.fromDateString("2018-12-22T00:00:00.000Z") 78 | ); 79 | 80 | console.log(resultState.toString()); 81 | // => [J2000] 82 | // Epoch: 2018-12-22T00:00:00.000Z 83 | // Position: [ -212.131159564, -2464.369431512, 6625.894946242 ] km 84 | // Velocity: [ -3.618619589, -6.126775239, -2.389579324 ] km/s 85 | 86 | // calculate the distance between result and expected, in kilometers 87 | const distance = resultState.position.distance(expectedState.position); 88 | 89 | console.log((distance * 1000).toFixed(3) + " meters"); 90 | // => 22.495 meters 91 | 92 | //============================================================================== 93 | // propagate state vector (numerical two-body) 94 | //============================================================================== 95 | 96 | const twoBodyProp = new RungeKutta4Propagator(initialState); 97 | twoBodyProp.forceModel.setEarthGravity(0, 0); 98 | twoBodyProp.propagate(EpochUTC.fromDateString("2018-12-22T00:00:00.000Z")); 99 | 100 | console.log(twoBodyProp.state.toString()); 101 | // => [J2000] 102 | // Epoch: 2018-12-22T00:00:00.000Z 103 | // Position: [ -1241.886675379, -3977.873474857, 5689.561067368 ] km 104 | // Velocity: [ -3.502813706, -5.085314761, -4.303326274 ] km/s 105 | 106 | //============================================================================== 107 | // propagate classical elements (analytical two-body) 108 | //============================================================================== 109 | 110 | // convert j2000 state to classical elements 111 | const ceState = initialState.toClassicalElements(); 112 | 113 | // create a propagator using classical elements, and propagate 114 | const keplerProp = new KeplerPropagator(ceState); 115 | keplerProp.propagate(EpochUTC.fromDateString("2018-12-22T00:00:00.000Z")); 116 | 117 | console.log(keplerProp.state.toString()); 118 | // => [J2000] 119 | // Epoch: 2018-12-22T00:00:00.000Z 120 | // Position: [ -1241.885644014, -3977.871988515, 5689.562370838 ] km 121 | // Velocity: [ -3.502814112, -5.085316082, -4.303324341 ] km/s 122 | -------------------------------------------------------------------------------- /src/data/data-handler.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { ASEC2RAD } from "../math/constants"; 3 | import { Vector3D } from "../math/vector-3d"; 4 | import { EGM_96_DENORMALIZED } from "./values/egm96"; 5 | import { EXPONENTIAL_ATMOSPHERE } from "./values/exponential-atmosphere"; 6 | import { clearFinals, FINALS, sortFinals, zeroFinal } from "./values/finals"; 7 | import { LEAP_SECONDS } from "./values/leap-seconds"; 8 | 9 | export class DataHandler { 10 | /** 11 | * Fetch the number of leap seconds used in offset. 12 | * 13 | * @param jd julian date 14 | */ 15 | public static leapSecondsOffset(jd: number) { 16 | if (jd > LEAP_SECONDS[LEAP_SECONDS.length - 1][0]) { 17 | return LEAP_SECONDS[LEAP_SECONDS.length - 1][1]; 18 | } 19 | if (jd < LEAP_SECONDS[0][0]) { 20 | return 0; 21 | } 22 | for (let i = 0; i < LEAP_SECONDS.length - 2; i++) { 23 | if (LEAP_SECONDS[i][0] <= jd && jd < LEAP_SECONDS[i + 1][0]) { 24 | return LEAP_SECONDS[i][1]; 25 | } 26 | } 27 | return 0; 28 | } 29 | 30 | /** 31 | * Add a new entry to the leap seconds table. 32 | * 33 | * @param jd julian date 34 | * @param offset leap seconds offset, in seconds 35 | */ 36 | public static addLeapSecond(jd: number, offset: number) { 37 | LEAP_SECONDS.push([jd, offset]); 38 | } 39 | 40 | /** 41 | * Get finals data for a given MJD. 42 | * 43 | * @param mjd USNO modified julian date 44 | */ 45 | public static getFinalsData(mjd: number) { 46 | const fmjd = Math.floor(mjd); 47 | if ( 48 | FINALS.length === 0 || 49 | fmjd < FINALS[0].mjd || 50 | fmjd > FINALS[FINALS.length - 1].mjd 51 | ) { 52 | return zeroFinal(); 53 | } 54 | let low = 0; 55 | let high = FINALS.length - 1; 56 | while (low <= high) { 57 | const mid = Math.floor((high + low) / 2); 58 | const midVal = FINALS[mid].mjd; 59 | if (fmjd < midVal) { 60 | high = mid - 1; 61 | } else if (fmjd > midVal) { 62 | low = mid + 1; 63 | } else { 64 | return FINALS[mid]; 65 | } 66 | } 67 | return zeroFinal(); 68 | } 69 | 70 | /** 71 | * Cache IERS finals.all data. 72 | * 73 | * @param lines list of finals.all lines 74 | */ 75 | public static setFinalsData(lines: string[]) { 76 | clearFinals(); 77 | for (let line of lines) { 78 | const tLine = line.trimRight(); 79 | if (tLine.length <= 68) { 80 | continue; 81 | } 82 | const mjd = Math.floor(parseFloat(line.substring(7, 15))); 83 | const pmX = parseFloat(line.substring(18, 27)) * ASEC2RAD; 84 | const pmY = parseFloat(line.substring(37, 46)) * ASEC2RAD; 85 | const dut1 = parseFloat(line.substring(58, 68)); 86 | let lod = 0; 87 | let dPsi = 0; 88 | let dEps = 0; 89 | if (tLine.length >= 86) { 90 | lod = parseFloat(line.substring(79, 86)) * 1e-3; 91 | } 92 | if (tLine.length >= 125) { 93 | dPsi = parseFloat(line.substring(97, 106)) * ASEC2RAD; 94 | dEps = parseFloat(line.substring(116, 125)) * ASEC2RAD; 95 | } 96 | FINALS.push({ 97 | mjd: mjd, 98 | pmX: pmX, 99 | pmY: pmY, 100 | dut1: dut1, 101 | lod: lod, 102 | dPsi: dPsi, 103 | dEps: dEps 104 | }); 105 | } 106 | sortFinals(); 107 | } 108 | 109 | /** 110 | * Calculate the density of the Earth's atmosphere, in kg/m^3, for a given 111 | * position using the exponential atmospheric density model. 112 | * 113 | * @param position satellite position 3-vector, in kilometers 114 | */ 115 | public static getExpAtmosphericDensity(position: Vector3D): number { 116 | const rDist = position.magnitude() - EarthBody.RADIUS_EQUATOR; 117 | const expAtm = EXPONENTIAL_ATMOSPHERE; 118 | const maxVal = expAtm.length - 1; 119 | let fields = [0.0, 0.0, 0.0]; 120 | if (rDist <= expAtm[0][0]) { 121 | fields = expAtm[0]; 122 | } else if (rDist >= expAtm[maxVal][0]) { 123 | fields = expAtm[maxVal]; 124 | } else { 125 | for (let i = 0; i < maxVal; i++) { 126 | if (expAtm[i][0] <= rDist && rDist < expAtm[i + 1][0]) { 127 | fields = expAtm[i]; 128 | } 129 | } 130 | } 131 | const base = fields[0]; 132 | const density = fields[1]; 133 | const height = fields[2]; 134 | return density * Math.exp(-(rDist - base) / height); 135 | } 136 | 137 | /** Calculate appropritate 1D index for 2D coefficient lookup. */ 138 | private static egm96Index(l: number, m: number) { 139 | return ((l - 2) * (l + 2) + l) / 2 - 1 + m; 140 | } 141 | 142 | /** 143 | * Lookup denormalized EGM96 coefficients. 144 | * 145 | * @param l first P index 146 | * @param m second P index 147 | */ 148 | public static getEgm96Coeffs(l: number, m: number) { 149 | return EGM_96_DENORMALIZED[DataHandler.egm96Index(l, m)]; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/forces/earth-gravity.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { ITRF } from "../coordinates/itrf"; 3 | import { J2000 } from "../coordinates/j2000"; 4 | import { DataHandler } from "../data/data-handler"; 5 | import { SphericalHarmonics } from "../math/spherical-harmonics"; 6 | import { Vector3D } from "../math/vector-3d"; 7 | import { AccelerationForce, AccelerationMap } from "./forces-interface"; 8 | 9 | /** Model of Earth gravity, for use in a ForceModel object. */ 10 | export class EarthGravity implements AccelerationForce { 11 | /** model aspherical gravity, if true. */ 12 | private earthAsphericalFlag: boolean; 13 | /** geopotential degree (max=70) */ 14 | private degree: number; 15 | /** geopotential order (max=70) */ 16 | private order: number; 17 | /** spherical harmonics manager */ 18 | private harmonics: SphericalHarmonics; 19 | 20 | /** 21 | * Create a new EarthGravity object. 22 | * 23 | * @param degree geopotential degree (max=70) 24 | * @param order geopotential order (max=70) 25 | */ 26 | constructor(degree: number, order: number) { 27 | this.earthAsphericalFlag = degree >= 2; 28 | this.degree = degree; 29 | this.order = order; 30 | this.harmonics = new SphericalHarmonics(degree); 31 | } 32 | 33 | /** 34 | * Calculate a cache of recurring values for the geopotential model: 35 | * 36 | * [ 37 | * sin(m * lambda), 38 | * cos(m * lambda), 39 | * tan(phi) 40 | * ] 41 | * 42 | * @param m m-index 43 | * @param lambda longitude, in radians 44 | * @param phi geocentric latitude, in radians 45 | */ 46 | private recurExp( 47 | m: number, 48 | lambda: number, 49 | phi: number 50 | ): [number, number, number] { 51 | var smLam = Math.sin(m * lambda); 52 | var cmLam = Math.cos(m * lambda); 53 | var mtPhi = m * Math.tan(phi); 54 | return [smLam, cmLam, mtPhi]; 55 | } 56 | 57 | /** 58 | * Calculate R, Phi, and Lambda acceleration derivatives. 59 | * 60 | * @param phi geocentric latitude, in radians 61 | * @param lambda longitude, in radians 62 | * @param r radius (km) 63 | */ 64 | private calcGradient( 65 | phi: number, 66 | lambda: number, 67 | r: number 68 | ): [number, number, number] { 69 | const { degree, order, harmonics, recurExp } = this; 70 | let sumR = 0; 71 | let sumPhi = 0; 72 | let sumLambda = 0; 73 | for (let l = 2; l <= degree; l++) { 74 | for (let m = 0; m <= Math.min(l, order); m++) { 75 | const { clm, slm } = DataHandler.getEgm96Coeffs(l, m); 76 | const [smLam, cmLam, mtPhi] = recurExp(m, lambda, phi); 77 | // r derivative 78 | const aR = 79 | Math.pow(EarthBody.RADIUS_EQUATOR / r, l) * 80 | (l + 1) * 81 | harmonics.getP(l, m); 82 | const bR = clm * cmLam + slm * smLam; 83 | sumR += aR * bR; 84 | // phi derivative 85 | const aPhi = 86 | Math.pow(EarthBody.RADIUS_EQUATOR / r, l) * 87 | (harmonics.getP(l, m + 1) - mtPhi * harmonics.getP(l, m)); 88 | const bPhi = clm * cmLam + slm * smLam; 89 | sumPhi += aPhi * bPhi; 90 | // lambda derivative 91 | const aLambda = 92 | Math.pow(EarthBody.RADIUS_EQUATOR / r, l) * m * harmonics.getP(l, m); 93 | const bLambda = slm * cmLam - clm * smLam; 94 | sumLambda += aLambda * bLambda; 95 | } 96 | } 97 | const dR = -(EarthBody.MU / (r * r)) * sumR; 98 | const dPhi = (EarthBody.MU / r) * sumPhi; 99 | const dLambda = (EarthBody.MU / r) * sumLambda; 100 | return [dR, dPhi, dLambda]; 101 | } 102 | 103 | /** 104 | * Calculate acceleration due to Earth's gravity, assuming a spherical Earth. 105 | * 106 | * @param j2kState J2000 state vector 107 | */ 108 | private earthSpherical(j2kState: J2000) { 109 | const { position } = j2kState; 110 | const rMag = position.magnitude(); 111 | return position.scale(-EarthBody.MU / (rMag * rMag * rMag)); 112 | } 113 | 114 | /** 115 | * Calculate the aspherical components of acceleration due to Earth's gravity. 116 | * 117 | * @param j2kState J2000 state vector 118 | */ 119 | private earthAspherical(j2kState: J2000) { 120 | const itrf = j2kState.toITRF(); 121 | const { x: ri, y: rj, z: rk } = itrf.position; 122 | const p = Math.sqrt(ri * ri + rj * rj); 123 | const phi = Math.atan2(rk, p); 124 | const lambda = Math.atan2(rj, ri); 125 | const r = itrf.position.magnitude(); 126 | this.harmonics.buildCache(phi); 127 | const [dR, dPhi, dLambda] = this.calcGradient(phi, lambda, r); 128 | const r2 = r * r; 129 | const ri2 = ri * ri; 130 | const rj2 = rj * rj; 131 | const p1 = (1 / r) * dR - (rk / (r2 * Math.sqrt(ri2 + rj2))) * dPhi; 132 | const p2 = (1 / (ri2 + rj2)) * dLambda; 133 | const ai = p1 * ri - p2 * rj; 134 | const aj = p1 * rj + p2 * ri; 135 | const ak = (1 / r) * dR * rk + (Math.sqrt(ri2 + rj2) / r2) * dPhi; 136 | const accVec = new ITRF(j2kState.epoch, new Vector3D(ai, aj, ak)); 137 | return accVec.toJ2000().position; 138 | } 139 | 140 | /** 141 | * Update the acceleration map argument with a calculated "earth_gravity" 142 | * value, for the provided state vector. 143 | * 144 | * @param j2kState J2000 state vector 145 | * @param accMap acceleration map (km/s^2) 146 | */ 147 | public acceleration(j2kState: J2000, accMap: AccelerationMap) { 148 | accMap["earth_gravity"] = this.earthSpherical(j2kState); 149 | if (this.earthAsphericalFlag) { 150 | accMap["earth_gravity"] = accMap["earth_gravity"].add( 151 | this.earthAspherical(j2kState) 152 | ); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/coordinates/j2000.ts: -------------------------------------------------------------------------------- 1 | import { EarthBody } from "../bodies/earth-body"; 2 | import { DataHandler } from "../data/data-handler"; 3 | import { TWO_PI } from "../math/constants"; 4 | import { Vector3D } from "../math/vector-3d"; 5 | import { EpochUTC } from "../time/epoch-utc"; 6 | import { ClassicalElements } from "./classical-elements"; 7 | import { IStateVector } from "./coordinate-interface"; 8 | import { ITRF } from "./itrf"; 9 | import { RIC } from "./ric"; 10 | import { TEME } from "./teme"; 11 | 12 | /** Class representing J2000 (J2K) inertial coordinates. */ 13 | export class J2000 implements IStateVector { 14 | /** Satellite state epoch. */ 15 | public readonly epoch: EpochUTC; 16 | /** Position vector, in kilometers. */ 17 | public readonly position: Vector3D; 18 | /** Velocity vector, in kilometers per second. */ 19 | public readonly velocity: Vector3D; 20 | 21 | /** 22 | * Create a new J2000 object. 23 | * 24 | * @param epoch UTC epoch 25 | * @param position J2000 position, in kilometers 26 | * @param velocity J2000 velocity, in kilometers per second 27 | */ 28 | constructor(epoch: EpochUTC, position: Vector3D, velocity?: Vector3D) { 29 | this.epoch = epoch; 30 | this.position = position; 31 | this.velocity = velocity || Vector3D.origin(); 32 | } 33 | 34 | /** Return a string representation of this object. */ 35 | public toString(): string { 36 | const { epoch, position, velocity } = this; 37 | const output = [ 38 | "[J2000]", 39 | ` Epoch: ${epoch.toString()}`, 40 | ` Position: ${position.toString()} km`, 41 | ` Velocity: ${velocity.toString()} km/s` 42 | ]; 43 | return output.join("\n"); 44 | } 45 | 46 | /** Calculate this orbit's mechanical energy, in km^2/s^2 */ 47 | private mechanicalEnergy() { 48 | const r = this.position.magnitude(); 49 | const v = this.velocity.magnitude(); 50 | return (v * v) / 2.0 - EarthBody.MU / r; 51 | } 52 | 53 | /** Convert to Classical Orbit Elements. */ 54 | public toClassicalElements(): ClassicalElements { 55 | const { epoch, position: pos, velocity: vel } = this; 56 | var mu = EarthBody.MU; 57 | var energy = this.mechanicalEnergy(); 58 | var a = -(mu / (2 * energy)); 59 | var eVecA = pos.scale(Math.pow(vel.magnitude(), 2) - mu / pos.magnitude()); 60 | var eVecB = vel.scale(pos.dot(vel)); 61 | var eVec = eVecA.add(eVecB.negate()).scale(1.0 / mu); 62 | var e = eVec.magnitude(); 63 | var h = pos.cross(vel); 64 | var i = Math.acos(h.z / h.magnitude()); 65 | var n = new Vector3D(0, 0, 1).cross(h); 66 | var o = Math.acos(n.x / n.magnitude()); 67 | if (n.y < 0) { 68 | o = TWO_PI - o; 69 | } 70 | var w = Math.acos(n.dot(eVec) / (n.magnitude() * eVec.magnitude())); 71 | if (eVec.z < 0) { 72 | w = TWO_PI - w; 73 | } 74 | var v = Math.acos(eVec.dot(pos) / (eVec.magnitude() * pos.magnitude())); 75 | if (pos.dot(vel) < 0) { 76 | v = TWO_PI - v; 77 | } 78 | return new ClassicalElements(epoch, a, e, i, o, w, v); 79 | } 80 | 81 | /** Convert this to a ITRF state vector object. */ 82 | public toITRF() { 83 | const { epoch, position, velocity } = this; 84 | const prec = EarthBody.precession(epoch); 85 | const rMOD = position 86 | .rot3(-prec[0]) 87 | .rot2(prec[1]) 88 | .rot3(-prec[2]); 89 | const vMOD = velocity 90 | .rot3(-prec[0]) 91 | .rot2(prec[1]) 92 | .rot3(-prec[2]); 93 | const [dPsi, dEps, mEps] = EarthBody.nutation(epoch); 94 | const epsilon = mEps + dEps; 95 | const rTOD = rMOD 96 | .rot1(mEps) 97 | .rot3(-dPsi) 98 | .rot1(-epsilon); 99 | const vTOD = vMOD 100 | .rot1(mEps) 101 | .rot3(-dPsi) 102 | .rot1(-epsilon); 103 | const ast = epoch.gmstAngle() + dPsi * Math.cos(epsilon); 104 | const rPEF = rTOD.rot3(ast); 105 | const vPEF = vTOD.rot3(ast).add( 106 | EarthBody.getRotation(this.epoch) 107 | .negate() 108 | .cross(rPEF) 109 | ); 110 | const { pmX, pmY } = DataHandler.getFinalsData(epoch.toMjd()); 111 | const rITRF = rPEF.rot1(-pmY).rot2(-pmX); 112 | const vITRF = vPEF.rot1(-pmY).rot2(-pmX); 113 | return new ITRF(epoch, rITRF, vITRF); 114 | } 115 | 116 | /** Convert this to a TEME state vector object. */ 117 | public toTEME() { 118 | const { epoch, position, velocity } = this; 119 | const [zeta, theta, zed] = EarthBody.precession(epoch); 120 | const rMOD = position 121 | .rot3(-zeta) 122 | .rot2(theta) 123 | .rot3(-zed); 124 | const vMOD = velocity 125 | .rot3(-zeta) 126 | .rot2(theta) 127 | .rot3(-zed); 128 | const [dPsi, dEps, mEps] = EarthBody.nutation(epoch); 129 | const epsilon = mEps + dEps; 130 | const rTEME = rMOD 131 | .rot1(mEps) 132 | .rot3(-dPsi) 133 | .rot1(-epsilon) 134 | .rot3(dPsi * Math.cos(epsilon)); 135 | const vTEME = vMOD 136 | .rot1(mEps) 137 | .rot3(-dPsi) 138 | .rot1(-epsilon) 139 | .rot3(dPsi * Math.cos(epsilon)); 140 | return new TEME(this.epoch, rTEME, vTEME); 141 | } 142 | 143 | /** 144 | * Convert this to a RIC relative motion object. 145 | * 146 | * @param reference target state for reference frame 147 | */ 148 | public toRIC(reference: J2000) { 149 | return RIC.fromJ2kState(this, reference); 150 | } 151 | 152 | /** 153 | * Apply an instantaneous delta-V to this state. 154 | * 155 | * Returns a new state object. 156 | * 157 | * @param radial radial delta-V (km/s) 158 | * @param intrack intrack delta-V (km/s) 159 | * @param crosstrack crosstrack delta-V (km/s) 160 | */ 161 | public maneuver(radial: number, intrack: number, crosstrack: number) { 162 | const ric = this.toRIC(this); 163 | return ric.addVelocity(radial, intrack, crosstrack).toJ2000(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/math/vector-3d.ts: -------------------------------------------------------------------------------- 1 | import { Vector6D } from "./vector-6d"; 2 | 3 | /** Class representing a vector of length 3. */ 4 | export class Vector3D { 5 | /** Vector x-axis component. */ 6 | public readonly x: number; 7 | /** Vector y-axis component. */ 8 | public readonly y: number; 9 | /** Vector z-axis component. */ 10 | public readonly z: number; 11 | 12 | /** 13 | * Create a new Vector3D object. 14 | * 15 | * @param x x-axis component 16 | * @param y y-axis component 17 | * @param z z-axis component 18 | */ 19 | constructor(x: number, y: number, z: number) { 20 | this.x = x; 21 | this.y = y; 22 | this.z = z; 23 | } 24 | 25 | /** 26 | * Create a new Vector3D object, containing zero for each state element. 27 | */ 28 | public static origin(): Vector3D { 29 | return new Vector3D(0, 0, 0); 30 | } 31 | 32 | /** Return a string representation of this vector. */ 33 | public toString(): string { 34 | const { x, y, z } = this; 35 | const xStr = x.toFixed(9); 36 | const yStr = y.toFixed(9); 37 | const zStr = z.toFixed(9); 38 | return `[ ${xStr}, ${yStr}, ${zStr} ]`; 39 | } 40 | 41 | /** Return the magnitude of this object. */ 42 | public magnitude(): number { 43 | const { x, y, z } = this; 44 | return Math.sqrt(x * x + y * y + z * z); 45 | } 46 | 47 | /** 48 | * Calculate the Euclidean distance between this and another Vector3D. 49 | * 50 | * @param v the other vector 51 | */ 52 | public distance(v: Vector3D): number { 53 | const { x, y, z } = this; 54 | var dx = x - v.x; 55 | var dy = y - v.y; 56 | var dz = z - v.z; 57 | return Math.sqrt(dx * dx + dy * dy + dz * dz); 58 | } 59 | 60 | /** 61 | * Perform element-wise addition of this and another Vector. 62 | * 63 | * Returns a new Vector object containing the sum. 64 | * 65 | * @param v the other vector 66 | */ 67 | public add(v: Vector3D): Vector3D { 68 | const { x, y, z } = this; 69 | return new Vector3D(x + v.x, y + v.y, z + v.z); 70 | } 71 | 72 | /** 73 | * Linearly scale the elements of this. 74 | * 75 | * Returns a new Vector object containing the scaled state. 76 | * 77 | * @param n scalar value 78 | */ 79 | public scale(n: number): Vector3D { 80 | const { x, y, z } = this; 81 | return new Vector3D(x * n, y * n, z * n); 82 | } 83 | 84 | /** Return a new Vector3D object with all values negated. */ 85 | public negate() { 86 | return this.scale(-1); 87 | } 88 | 89 | /** 90 | * Return the normalized (unit vector) form of this as a new Vector3D object. 91 | */ 92 | public normalized(): Vector3D { 93 | const { x, y, z } = this; 94 | const m = this.magnitude(); 95 | return new Vector3D(x / m, y / m, z / m); 96 | } 97 | 98 | /** 99 | * Calculate the cross product of this and another Vector. 100 | * 101 | * Returns the result as a new Vector object. 102 | * 103 | * @param v the other vector 104 | */ 105 | public cross(v: Vector3D): Vector3D { 106 | const { x, y, z } = this; 107 | return new Vector3D( 108 | y * v.z - z * v.y, 109 | z * v.x - x * v.z, 110 | x * v.y - y * v.x 111 | ); 112 | } 113 | 114 | /** 115 | * Calculate the dot product this and another Vector. 116 | * 117 | * @param v the other vector 118 | */ 119 | public dot(v: Vector3D): number { 120 | const { x, y, z } = this; 121 | return x * v.x + y * v.y + z * v.z; 122 | } 123 | 124 | /** 125 | * Rotate the elements of this along the x-axis. 126 | * 127 | * @param theta rotation angle, in radians 128 | */ 129 | public rot1(theta: number): Vector3D { 130 | const cosT = Math.cos(theta); 131 | const sinT = Math.sin(theta); 132 | const { x, y, z } = this; 133 | return new Vector3D( 134 | 1 * x + 0 * y + 0 * z, 135 | 0 * x + cosT * y + sinT * z, 136 | 0 * x + -sinT * y + cosT * z 137 | ); 138 | } 139 | 140 | /** 141 | * Rotate the elements of this along the y-axis. 142 | * 143 | * @param theta rotation angle, in radians 144 | */ 145 | public rot2(theta: number): Vector3D { 146 | const cosT = Math.cos(theta); 147 | const sinT = Math.sin(theta); 148 | const { x, y, z } = this; 149 | return new Vector3D( 150 | cosT * x + 0 * y + -sinT * z, 151 | 0 * x + 1 * y + 0 * z, 152 | sinT * x + 0 * y + cosT * z 153 | ); 154 | } 155 | 156 | /** 157 | * Rotate the elements of this along the z-axis. 158 | * 159 | * @param theta rotation angle, in radians 160 | */ 161 | public rot3(theta: number): Vector3D { 162 | const cosT = Math.cos(theta); 163 | const sinT = Math.sin(theta); 164 | const { x, y, z } = this; 165 | return new Vector3D( 166 | cosT * x + sinT * y + 0 * z, 167 | -sinT * x + cosT * y + 0 * z, 168 | 0 * x + 0 * y + 1 * z 169 | ); 170 | } 171 | 172 | /** 173 | * Calculate the angle, in radians, between this and another Vector3D. 174 | * 175 | * @param v the other vector 176 | */ 177 | public angle(v: Vector3D): number { 178 | return Math.acos(this.dot(v) / (this.magnitude() * v.magnitude())); 179 | } 180 | 181 | /** 182 | * Change coordinates of this to the relative position from a new origin. 183 | * 184 | * @param origin new origin 185 | */ 186 | public changeOrigin(origin: Vector3D): Vector3D { 187 | const delta = origin.negate(); 188 | return this.add(delta); 189 | } 190 | 191 | /** 192 | * Join this and another Vector3D object into a single Vector6D object. 193 | * 194 | * @param v other vector 195 | */ 196 | public join(v: Vector3D) { 197 | const { x: a, y: b, z: c } = this; 198 | const { x, y, z } = v; 199 | return new Vector6D(a, b, c, x, y, z); 200 | } 201 | 202 | /** 203 | * Determine line of sight between two vectors and the radius of a central 204 | * object. Returns true if line-of-sight exists. 205 | * 206 | * @param v other vector 207 | * @param radius central body radius 208 | */ 209 | public sight(v: Vector3D, radius: number) { 210 | const r1Mag2 = Math.pow(this.magnitude(), 2); 211 | const r2Mag2 = Math.pow(v.magnitude(), 2); 212 | const rDot = this.dot(v); 213 | let los = false; 214 | const tMin = (r1Mag2 - rDot) / (r1Mag2 + r2Mag2 - 2 * rDot); 215 | if (tMin < 0 || tMin > 1) { 216 | los = true; 217 | } else { 218 | const c = (1 - tMin) * r1Mag2 + rDot * tMin; 219 | if (c >= radius * radius) { 220 | los = true; 221 | } 222 | } 223 | return los; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Pious Squid 2 | 3 | [![npm version](https://badge.fury.io/js/pious-squid.svg)](https://badge.fury.io/js/pious-squid) 4 | 5 | Orbital mechanics and satellite mission analysis library, for NodeJS and the 6 | browser. 7 | 8 | ## Features 9 | 10 | - **Coordinate Frames** 11 | 12 | - Classical Orbit Elements 13 | - Earth Centered Earth Fixed _(ITRF)_ 14 | - Geodetic 15 | - J2000 16 | - Look Angles 17 | - True Equator Mean Equinox _(TEME)_ 18 | - Relative Motion _(RIC)_ 19 | 20 | - **Ephemeris Propagators** 21 | 22 | - 4th Order Runge-Kutta _(numerical)_ 23 | - Keplerian _(analytic)_ 24 | - Interpolator 25 | 26 | - **Celestial Bodies** 27 | 28 | - Earth Atmospheric Density 29 | - Earth Precession / Nutation 30 | - Moon Position 31 | - Solar Eclipse 32 | - Sun Position 33 | 34 | - **Epoch** 35 | 36 | - Barycentric Dynamical Time _(TDB)_ 37 | - Greenwich Mean Sidereal Time 38 | - International Atomic Time _(TAI)_ 39 | - Julian Centuries 40 | - Julian Date 41 | - Leap Seconds 42 | - Terrestrial Time _(TT)_ 43 | - UTC/UT1 Time 44 | 45 | - **Force Model** 46 | 47 | - Atmospheric Drag 48 | - Earth Geopotential _(70x70)_ 49 | - Moon Gravity 50 | - Solar Radiation Pressure 51 | - Sun Gravity 52 | 53 | - **Operations** 54 | 55 | - Satellite Maneuvers 56 | - Monte-Carlo Simulation 57 | - Conjunction Assesment 58 | 59 | ## Install 60 | 61 | To include `pious-squid` in your _NodeJS_ project: 62 | 63 | npm install pious-squid --save 64 | 65 | The browser library bundles (`pious-squid.js` or `pious-squid.min.js`) can be 66 | found under, the 67 | [Releases](https://github.com/david-rc-dayton/pious-squid/releases) 68 | tab on _GitHub_. 69 | 70 | ## Example 71 | 72 | To propagate a satellite from its position and velocity vectors: 73 | 74 | ```javascript 75 | import { 76 | EpochUTC, 77 | Geodetic, 78 | J2000, 79 | RungeKutta4Propagator, 80 | Vector3D 81 | } from "pious-squid"; 82 | 83 | // ============================================================================= 84 | // set initial state in J2000 frame 85 | // ============================================================================= 86 | 87 | const initialState = new J2000( 88 | EpochUTC.fromDateString("2018-12-21T00:00:00.000Z"), // epoch (UTC) 89 | new Vector3D(-1117.913276, 73.093299, -7000.018272), // km 90 | new Vector3D(3.531365461, 6.583914964, -0.495649656) // km/s 91 | ); 92 | 93 | console.log(initialState.toString()); 94 | // => [J2000] 95 | // Epoch: 2018-12-21T00:00:00.000Z 96 | // Position: [ -1117.913276000, 73.093299000, -7000.018272000 ] km 97 | // Velocity: [ 3.531365461, 6.583914964, -0.495649656 ] km/s 98 | 99 | // ============================================================================= 100 | // create a propagator object 101 | // ============================================================================= 102 | 103 | const propagator = new RungeKutta4Propagator(initialState); 104 | 105 | // set the step size 106 | propagator.setStepSize(5); // seconds 107 | 108 | // add Earth gravity acceleration 109 | propagator.forceModel.setEarthGravity( 110 | 50, // degree 111 | 50 // order 112 | ); 113 | 114 | // add third-body acceleration 115 | propagator.forceModel.setThirdBody( 116 | true, // moon gravity 117 | true // sun gravity 118 | ); 119 | 120 | // ============================================================================= 121 | // propagate ephemeris to a future time 122 | // ============================================================================= 123 | 124 | const finalState = propagator.propagate( 125 | EpochUTC.fromDateString("2018-12-22T00:00:00.000Z") 126 | ); 127 | 128 | console.log(finalState.toString()); 129 | // => [J2000] 130 | // Epoch: 2018-12-22T00:00:00.000Z 131 | // Position: [ -212.111629987, -2464.336270508, 6625.907441304 ] km 132 | // Velocity: [ -3.618621245, -6.126790740, -2.389539402 ] km/s 133 | 134 | // ============================================================================= 135 | // display information about the propagated state 136 | // ============================================================================= 137 | 138 | // Earth-fixed coordinates 139 | console.log(finalState.toITRF().toString()); 140 | // => [ITRF] 141 | // Epoch: 2018-12-22T00:00:00.000Z 142 | // Position: [ -2463.105532067, 235.348124556, 6625.580458844 ] km 143 | // Velocity: [ -6.093169860, 3.821763334, -2.395927109 ] km/s 144 | 145 | // geodetic coordinates 146 | console.log( 147 | finalState 148 | .toITRF() 149 | .toGeodetic() 150 | .toString() 151 | ); 152 | // => [Geodetic] 153 | // Latitude: 69.635° 154 | // Longitude: 174.542° 155 | // Altitude: 713.165 km 156 | 157 | // look angle from ground observer 158 | const observer = new Geodetic( 159 | 71.218 * (Math.PI / 180), // latitude (radians) 160 | 180.508 * (Math.PI / 180), // longitude (radians) 161 | 0.325 // altitude (km) 162 | ); 163 | console.log( 164 | finalState 165 | .toITRF() 166 | .toLookAngle(observer) 167 | .toString() 168 | ); 169 | // => [Look-Angle] 170 | // Azimuth: 234.477° 171 | // Elevation: 65.882° 172 | // Range: 773.318 km 173 | 174 | // relative position 175 | const actualState = new J2000( 176 | EpochUTC.fromDateString("2018-12-22T00:00:00.000Z"), 177 | new Vector3D(-212.125533, -2464.351601, 6625.907454), 178 | new Vector3D(-3.618617698, -6.12677853, -2.38955619) 179 | ); 180 | console.log(finalState.toRIC(actualState).toString()); 181 | // => [RIC] 182 | // Epoch: 2018-12-22T00:00:00.000Z 183 | // Position: [ -0.005770585, -0.019208198, 0.005105235 ] km 184 | // Velocity: [ 0.000020089, 0.000006319, 0.000000117 ] km/s 185 | ``` 186 | 187 | Additional examples can be found in the 188 | [examples](https://github.com/david-rc-dayton/pious-squid/tree/master/src/examples) 189 | directory in the project source directory. 190 | 191 | ## License 192 | 193 | **The MIT License (MIT)** 194 | 195 | Copyright © 2018 David RC Dayton 196 | 197 | Permission is hereby granted, free of charge, to any person obtaining a copy of 198 | this software and associated documentation files (the “Software”), to deal in 199 | the Software without restriction, including without limitation the rights to 200 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 201 | of the Software, and to permit persons to whom the Software is furnished to do 202 | so, subject to the following conditions: 203 | 204 | The above copyright notice and this permission notice shall be included in all 205 | copies or substantial portions of the Software. 206 | 207 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 208 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 209 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 210 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 211 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 212 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 213 | SOFTWARE. 214 | --------------------------------------------------------------------------------