UNPKG

ootk

Version:

Orbital Object Toolkit including Multiple Propagators, Initial Orbit Determination, and Maneuver Calculations.

233 lines (190 loc) 7.35 kB
/** * @author @thkruz Theodore Kruczek * @license AGPL-3.0-or-later * @copyright (c) 2025 Kruczek Labs LLC * * Orbital Object ToolKit is free software: you can redistribute it and/or modify it under the * terms of the GNU Affero General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * Orbital Object ToolKit is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with * Orbital Object ToolKit. If not, see <http://www.gnu.org/licenses/>. */ import { EpochUTC, J2000, Kilometers, KilometersPerSecond, Seconds, Vector, Vector3D } from '../main.js'; import { ForceModel } from '../force/ForceModel.js'; import { Thrust } from '../force/Thrust.js'; import { VerletBlendInterpolator } from './../interpolator/VerletBlendInterpolator.js'; import { Propagator } from './Propagator.js'; import { RkCheckpoint } from './RkCheckpoint.js'; import { RkResult } from './RkResult.js'; // / Adaptive Runge-Kutta propagator base class. export abstract class RungeKuttaAdaptive extends Propagator { /** * Create a new [RungeKuttaAdaptive] object from an initial state vector * along with an optional [ForceModel] and [tolerance]. * @param initState_ Initial state vector. * @param forceModel_ Numerical integration force model. * @param tolerance_ Minimum allowable local error tolerance. */ constructor( private readonly initState_: J2000, private forceModel_: ForceModel = new ForceModel().setGravity(), private readonly tolerance_: number = 1e-9, ) { super(); this._cacheState = this.initState_; this.tolerance_ = Math.max(RungeKuttaAdaptive._minTolerance, Math.abs(tolerance_)); } // / Initial state vector. private _cacheState: J2000; private readonly _checkpoints: RkCheckpoint[] = []; // / Integration step size _(seconds)_. private _stepSize = 60.0; // / Minimum allowable local error tolerance. private static readonly _minTolerance: number = 1e-15; // / Butcher tableau `A` values. protected abstract get a(): Float64Array; // / Butcher tableau `B` values. protected abstract get b(): Float64Array[]; // / Butcher tableau `CH` values. protected abstract get ch(): Float64Array; // / Butcher tableau `C` values protected abstract get c(): Float64Array; // / Integrator order. protected abstract get order(): number; get state(): J2000 { return this._cacheState; } reset(): void { this._cacheState = this.initState_; this._stepSize = 60.0; } // / Set numerical integration force model. setForceModel(forceModel: ForceModel): void { this.forceModel_ = forceModel; } private kfn_( epoch: EpochUTC, rv: Vector<Kilometers|KilometersPerSecond>, hArg: Seconds, kArg: Vector<Kilometers>, step: Seconds, ): Vector { const t = epoch.roll(hArg * step as Seconds); const rvNew = rv.add(kArg); const sample = new J2000( t, rvNew.toVector3D(0) as Vector3D<Kilometers>, rvNew.toVector3D(3) as Vector3D<KilometersPerSecond>, ); return this.forceModel_.derivative(sample).scale(step); } private integrate_(state: J2000, step: Seconds): RkResult { const k: Vector[] = new Array(this.a.length).fill(Vector.origin3); const y = state.position.join(state.velocity) as Vector<Kilometers>; for (let i = 0; i < this.a.length; i++) { let kArg = Vector.origin6 as Vector<Kilometers>; if (i !== 0) { for (let j = 0; j < i; j++) { kArg = kArg.add(k[j].scale(this.b[i][j])) as Vector<Kilometers>; } } k[i] = this.kfn_(state.epoch, y, this.a[i] as Seconds, kArg, step); } let y1 = y; let y2 = y; for (let i = 0; i < k.length; i++) { y1 = y1.add(k[i].scale(this.ch[i])) as Vector<Kilometers>; y2 = y2.add(k[i].scale(this.c[i])) as Vector<Kilometers>; } const teVal = y1.distance(y2); let hNew = 0.9 * step * (this.tolerance_ / teVal) ** (1.0 / this.order); const hOld = Math.abs(step); hNew = Math.max(0.2 * hOld, Math.min(5.0 * hOld, hNew)); hNew = Math.max(1e-5, Math.min(1000.0, hNew)); return new RkResult( new J2000( state.epoch.roll(step), y1.toVector3D(0) as Vector3D<Kilometers>, y1.toVector3D(3) as Vector3D<KilometersPerSecond>, ), teVal, hNew, ); } propagate(epoch: EpochUTC): J2000 { let delta = epoch.difference(this._cacheState.epoch); while (delta !== 0) { const direction = delta >= 0 ? 1 : -1; const dt = Math.min(Math.abs(delta), this._stepSize) * direction as Seconds; const result = this.integrate_(this._cacheState, dt); this._stepSize = result.newStep; if (result.error > this.tolerance_) { continue; } this._cacheState = result.state; delta = epoch.difference(this._cacheState.epoch); } return this._cacheState; } maneuver(maneuver: Thrust, interval = 60.0 as Seconds): J2000[] { if (maneuver.isImpulsive) { this._cacheState = maneuver.apply(this.propagate(maneuver.center)); return [this._cacheState]; } let tState = this.propagate(maneuver.start); this.forceModel_.loadManeuver(maneuver); const ephemeris: J2000[] = [tState]; while (tState.epoch < maneuver.stop) { const step = Math.min(maneuver.stop.difference(tState.epoch), interval) as Seconds; tState = this.propagate(tState.epoch.roll(step)); ephemeris.push(tState); } this.forceModel_.clearManeuver(); return ephemeris; } ephemerisManeuver( start: EpochUTC, finish: EpochUTC, maneuvers: Thrust[], interval = 60.0 as Seconds, ): VerletBlendInterpolator { const tMvr = maneuvers.filter((mvr) => mvr.start >= start || mvr.stop <= finish); const ephemeris: J2000[] = []; if (tMvr[0].start > start) { ephemeris.push(this.propagate(start)); } for (const mvr of tMvr) { while (this._cacheState.epoch < mvr.start) { const step = Math.min(mvr.start.difference(this._cacheState.epoch), interval) as Seconds; this.propagate(this._cacheState.epoch.roll(step)); if (this._cacheState.epoch.posix !== mvr.start.posix) { ephemeris.push(this._cacheState); } } ephemeris.push(...this.maneuver(mvr, interval)); } while (this._cacheState.epoch.posix < finish.posix) { const step = Math.min(finish.difference(this._cacheState.epoch), interval) as Seconds; this.propagate(this._cacheState.epoch.roll(step)); ephemeris.push(this._cacheState); } return new VerletBlendInterpolator(ephemeris); } checkpoint(): number { this._checkpoints.push(new RkCheckpoint(this._cacheState, this._stepSize)); return this._checkpoints.length - 1; } clearCheckpoints(): void { this._checkpoints.length = 0; } restore(index: number): void { const checkpoint = this._checkpoints[index]; this._cacheState = checkpoint.cacheState; this._stepSize = checkpoint.stepSize; } }