ootk
Version:
Orbital Object Toolkit including Multiple Propagators, Initial Orbit Determination, and Maneuver Calculations.
233 lines (190 loc) • 7.35 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';
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;
}
}