ootk
Version:
Orbital Object Toolkit including Multiple Propagators, Initial Orbit Determination, and Maneuver Calculations.
268 lines (246 loc) • 9.77 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,
MetersPerSecond,
RIC,
SecondsPerMeterPerSecond,
Vector3D,
} from '../main.js';
import { Thrust } from '../force/Thrust.js';
import { StateInterpolator } from '../interpolator/StateInterpolator.js';
import { ForceModel } from './../force/ForceModel.js';
import { DownhillSimplex } from './../optimize/DownhillSimplex.js';
import { LambertIOD } from './../orbit_determination/LambertIOD.js';
import { RungeKutta89Propagator } from './../propagator/RungeKutta89Propagator.js';
// / Relative waypoint targeting.
export class Waypoint {
epoch: EpochUTC;
relativePosition: Vector3D<Kilometers>;
// / Create a new [Waypoint] object.
constructor(epoch: EpochUTC, relativePosition: Vector3D<Kilometers>) {
this.epoch = epoch;
this.relativePosition = relativePosition;
}
/**
* Return the perturbed error in a [maneuver] when compared against the
* target [waypoint] given an initial [state], [forceModel],
* [target] interpolator, and speculative relative maneuver
* [components] _(m/s)_.
* @param waypoint The waypoint to target.
* @param maneuver The maneuver to perturb.
* @param state The initial state of the interceptor.
* @param forceModel The force model to use for propagation.
* @param target The target interpolator.
* @param components The speculative maneuver components.
* @returns The perturbed error in the maneuver.
*/
static _error(
waypoint: Waypoint,
maneuver: Thrust,
state: J2000,
forceModel: ForceModel,
target: StateInterpolator,
components: Float64Array, // MetersPerSecond
): number {
const testManeuver = new Thrust(
maneuver.center,
components[0] as MetersPerSecond,
components[1] as MetersPerSecond,
components[2] as MetersPerSecond,
maneuver.durationRate,
);
const propagator = new RungeKutta89Propagator(state, forceModel);
const maneuverSteps = propagator.maneuver(testManeuver);
const postManeuver = maneuverSteps[maneuverSteps.length - 1];
const interceptor = new RungeKutta89Propagator(postManeuver, forceModel).propagate(waypoint.epoch);
const targetState = target.interpolate(waypoint.epoch);
if (targetState === null) {
throw new Error('Error calculation failed; epoch is outside the target interpolator ephemeris window.');
}
const expected = waypoint.relativePosition;
const actual = RIC.fromJ2000(interceptor, targetState);
return actual.position.distance(expected);
}
/**
* Generate a score function for refining perturbed [waypoint] maneuvers.
*
* The score function takes an array of speculative radial, intrack, and
* crosstrack components _(m/s)_ and returns the propagated error from the
* desired waypoint target.
* @param waypoint The waypoint to target.
* @param maneuver The maneuver to perturb.
* @param state The initial state of the interceptor.
* @param forceModel The force model to use for propagation.
* @param target The target interpolator.
* @returns A score function for refining maneuvers.
*/
static _refineManeuverScore(
waypoint: Waypoint,
maneuver: Thrust,
state: J2000,
forceModel: ForceModel,
target: StateInterpolator,
): (components: Float64Array) => number {
return (components: Float64Array) => Waypoint._error(waypoint, maneuver, state, forceModel, target, components);
}
/**
* Convert an array of [waypoints] into a maneuver sequence given an
* [interceptor] state, [pivot] epoch for the first burn to arrive at the
* first waypoint, and [target] ephemeris interpolator.
*
* Optional arguments are as follows:
* - `preManeuvers`: maneuvers to execute before the pivot burn
* - `postManeuvers`: maneuvers to execute after the last pivot burn
* - `durationRate`: thruster duration rate _(s/m/s)_
* - `forceModel`: interceptor force model, defaults to two-body
* - `refine`: refine maneuvers to account for perturbations if `true`
* - `maxIter`: maximum refinement iterations per maneuver
* - `printIter`: print debug information on each refinement iteration
* @param interceptor The interceptor state.
* @param pivot The epoch of the first burn.
* @param waypoints The waypoints to target.
* @param target The target interpolator.
* @param preManeuvers The maneuvers to execute before the pivot burn.
* @param postManeuvers The maneuvers to execute after the last pivot burn.
* @param root0 The optional arguments.
* @param root0.durationRate The thruster duration rate.
* @param root0.forceModel The interceptor force model.
* @param root0.refine Whether to refine maneuvers to account for perturbations.
* @param root0.maxIter The maximum refinement iterations per maneuver.
* @param root0.printIter Whether to print debug information on each refinement iteration.
* @returns An array of maneuvers.
*/
static toManeuvers(
interceptor: J2000,
pivot: EpochUTC,
waypoints: Waypoint[],
target: StateInterpolator,
preManeuvers: Thrust[] | null,
postManeuvers: Thrust[] | null,
{
durationRate = 0.0,
forceModel,
refine = false,
maxIter = 500,
printIter = false,
}: {
durationRate?: number;
forceModel?: ForceModel;
refine?: boolean;
maxIter?: number;
printIter?: boolean;
} = {},
): Thrust[] {
const preMnv = preManeuvers ?? [];
const postMnv = postManeuvers ?? [];
let state = interceptor;
if (preMnv.length > 0) {
for (const maneuver of preMnv) {
const mvrStep = new RungeKutta89Propagator(state, forceModel).maneuver(maneuver);
state = mvrStep[mvrStep.length - 1];
}
}
state = new RungeKutta89Propagator(state, forceModel).propagate(pivot);
const pivotState = state;
let waypointManeuvers: Thrust[] = [];
for (const wp of waypoints) {
const targetWp = new RIC(wp.relativePosition, Vector3D.origin as Vector3D<KilometersPerSecond>);
const targetState = target.interpolate(wp.epoch);
if (targetState === null) {
throw new Error('Waypoint outside target interpolator window.');
}
const wpState = targetWp.toJ2000(targetState);
const tof = wp.epoch.difference(state.epoch);
const revs = Math.floor(tof / state.period);
const shortPath = LambertIOD.useShortPath(state, targetState);
const lambert = new LambertIOD();
const components = lambert.estimate(state.position, wpState.position, state.epoch, wp.epoch, {
posigrade: shortPath,
nRev: revs,
});
if (components === null) {
throw new Error('Lambert solve result is null.');
}
const componentsRel = RIC.fromJ2000(components, state).velocity.scale(1e3) as Vector3D<MetersPerSecond>;
const maneuver = new Thrust(
state.epoch,
componentsRel.x,
componentsRel.y,
componentsRel.z,
durationRate as SecondsPerMeterPerSecond,
);
const tempProp = new RungeKutta89Propagator(state, forceModel);
tempProp.maneuver(maneuver);
state = tempProp.propagate(wp.epoch);
waypointManeuvers.push(maneuver);
}
if (refine) {
forceModel ??= new ForceModel().setGravity();
waypointManeuvers = this._refineManeuvers(waypoints, waypointManeuvers, pivotState, forceModel, target, {
maxIter,
printIter,
});
}
const output: Thrust[] = [];
output.push(...preMnv);
output.push(...waypointManeuvers);
output.push(...postMnv);
return output;
}
static _refineManeuvers(
waypoints: Waypoint[],
maneuvers: Thrust[],
interceptor: J2000,
forceModel: ForceModel,
target: StateInterpolator,
{
maxIter = 500,
printIter = false,
}: {
maxIter?: number;
printIter?: boolean;
} = {},
): Thrust[] {
let state = interceptor;
const output: Thrust[] = [];
for (let i = 0; i < maneuvers.length; i++) {
const maneuver = maneuvers[i];
const waypoint = waypoints[i];
const guess = maneuver.deltaV.scale(1e3).toArray();
const simplex = DownhillSimplex.generateSimplex(guess, 1e-1);
const scoreFn = this._refineManeuverScore(waypoint, maneuver, state, forceModel, target);
const results = DownhillSimplex.solveSimplex(scoreFn, simplex, {
maxIter,
xTolerance: 1e-6,
fTolerance: 1e-6,
printIter,
});
const tR = results[0] as MetersPerSecond;
const tI = results[1] as MetersPerSecond;
const tC = results[2] as MetersPerSecond;
const newManeuver = new Thrust(maneuver.center, tR, tI, tC, maneuver.durationRate);
output.push(newManeuver);
const mvrStep = new RungeKutta89Propagator(state, forceModel).maneuver(newManeuver);
state = mvrStep[mvrStep.length - 1];
}
return output;
}
}