ootk
Version:
Orbital Object Toolkit including Multiple Propagators, Initial Orbit Determination, and Maneuver Calculations.
180 lines (150 loc) • 6.01 kB
text/typescript
/**
* @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';
// / Runge-Kutta 4 fixed numerical propagator.
export class RungeKutta4Propagator extends Propagator {
/**
* Create a new [RungeKutta4Propagator] object from an initial state vector and
* along with an optional [ForceModel] and [stepSize] in seconds.
* @param initState_ Initial state vector.
* @param forceModel_ Numerical integration force model.
* @param stepSize_ Integration step size _(seconds)_.
* @param cacheState_ Cached state vector.
* @param checkpoints_ Cached state vector checkpoints.
*/
constructor(
private readonly initState_: J2000,
private forceModel_: ForceModel = new ForceModel().setGravity(),
private stepSize_: number = 15.0,
private cacheState_: J2000 = initState_,
private checkpoints_: J2000[] = [],
) {
super();
this.stepSize_ = Math.abs(stepSize_);
}
// / Set the integrator step size to the provided number of [seconds].
setStepSize(seconds: number): void {
this.stepSize_ = Math.abs(seconds);
}
// / Set numerical integration force model.
setForceModel(forceModel: ForceModel): void {
this.forceModel_ = forceModel;
}
ephemerisManeuver(
start: EpochUTC,
finish: EpochUTC,
maneuvers: Thrust[],
interval = 60.0 as Seconds,
): VerletBlendInterpolator {
const tMvr = maneuvers.slice(0).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);
}
maneuver(maneuver: Thrust, interval = 60.0): 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;
}
private _kFn(state: J2000, hArg: Seconds, kArg: Vector): Vector {
const epoch = state.epoch.roll(hArg);
const posvel = state.position.join(state.velocity);
const result = posvel.add(kArg);
const sample = new J2000(
epoch,
result.toVector3D(0) as Vector3D<Kilometers>,
result.toVector3D(3) as Vector3D<KilometersPerSecond>,
);
return this.forceModel_.derivative(sample);
}
private _integrate(state: J2000, step: Seconds): J2000 {
const k1 = this._kFn(state, 0 as Seconds, Vector.zero(6)).scale(step);
const k2 = this._kFn(state, 0.5 * step as Seconds, k1.scale(0.5)).scale(step);
const k3 = this._kFn(state, 0.5 * step as Seconds, k2.scale(0.5)).scale(step);
const k4 = this._kFn(state, step, k3).scale(step);
const v1 = k1;
const v2 = v1.add(k2.scale(2));
const v3 = v2.add(k3.scale(2));
const v4 = v3.add(k4);
const tNext = state.epoch.roll(step);
const posvel = state.position.join(state.velocity);
const result = posvel.add(v4.scale(1 / 6));
return new J2000(
tNext,
result.toVector3D(0) as Vector3D<Kilometers>,
result.toVector3D(3) as Vector3D<KilometersPerSecond>,
);
}
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;
this.cacheState_ = this._integrate(this.cacheState_, dt);
delta = epoch.difference(this.cacheState_.epoch);
}
return this.cacheState_;
}
reset(): void {
this.cacheState_ = this.initState_;
}
get state(): J2000 {
return this.cacheState_;
}
checkpoint(): number {
this.checkpoints_.push(this.cacheState_);
return this.checkpoints_.length - 1;
}
clearCheckpoints(): void {
this.checkpoints_ = [];
}
restore(index: number): void {
this.cacheState_ = this.checkpoints_[index];
}
}