ootk
Version:
Orbital Object Toolkit including Multiple Propagators, Initial Orbit Determination, and Maneuver Calculations.
175 lines (155 loc) • 6.43 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 { concat, EpochUTC, J2000, Kilometers, KilometersPerSecond, Matrix, Vector, Vector3D } from '../main.js';
import { Observation } from '../observation/Observation.js';
import { PropagatorPairs } from '../observation/PropagatorPairs.js';
import { Propagator } from '../propagator/Propagator.js';
import { CovarianceFrame, StateCovariance } from './../covariance/StateCovariance.js';
import { ForceModel } from './../force/ForceModel.js';
import { KeplerPropagator } from './../propagator/KeplerPropagator.js';
import { RungeKutta89Propagator } from './../propagator/RungeKutta89Propagator.js';
import { BatchLeastSquaresResult } from './BatchLeastSquaresResult.js';
/**
* Batch least squares orbit determination.
*/
export class BatchLeastSquaresOD {
/** Propagator pair cache, for generating observation Jacobians. */
private readonly propPairs_: PropagatorPairs;
/** Nominal state propagator. */
private propagator_: Propagator;
/** State estimate during solve. */
private readonly nominal_: J2000;
/** Solve start epoch. */
private readonly start_: EpochUTC;
/**
* Create a new [BatchLeastSquaresOD] object from a list of [Observation]
* objects, an [apriori] state estimate, and an optional
* spacecraft [forceModel].
* @param observations_ List of observations.
* @param apriori_ Apriori state estimate.
* @param forceModel_ Spacecraft force model.
* @param posStep_ Position step size.
* @param velStep_ Velocity step size.
* @param fastDerivatives_ Use fast derivatives.
* @returns [BatchLeastSquaresOD] object.
*/
constructor(
private readonly observations_: Observation[],
private readonly apriori_: J2000,
private readonly forceModel_?: ForceModel,
private readonly posStep_: number = 1e-5,
private readonly velStep_: number = 1e-5,
private readonly fastDerivatives_: boolean = false,
) {
this.observations_.sort((a, b) => a.epoch.posix - b.epoch.posix);
this.start_ = this.observations_[0].epoch;
this.propPairs_ = new PropagatorPairs(this.posStep_, this.velStep_);
this.forceModel_ ??= new ForceModel().setGravity();
this.propagator_ = new RungeKutta89Propagator(this.apriori_, this.forceModel_);
this.nominal_ = this.propagator_.propagate(this.start_);
}
private buildPropagator_(x0: Float64Array, simple: boolean): Propagator {
const state = new J2000(
this.nominal_.epoch,
new Vector3D(x0[0] as Kilometers, x0[1] as Kilometers, x0[2] as Kilometers),
new Vector3D(x0[3] as KilometersPerSecond, x0[4] as KilometersPerSecond, x0[5] as KilometersPerSecond),
);
if (simple) {
return new KeplerPropagator(state.toClassicalElements());
}
return new RungeKutta89Propagator(state, this.forceModel_);
}
private static stateToX0_(state: J2000): Float64Array {
return concat(state.position.toArray(), state.velocity.toArray());
}
private setPropagatorPairs_(x0: Float64Array): void {
const pl = this.buildPropagator_(x0, this.fastDerivatives_);
for (let i = 0; i < 6; i++) {
const step = this.propPairs_.step(i);
const xh = x0.slice();
xh[i] += step;
const ph = this.buildPropagator_(xh, this.fastDerivatives_);
this.propPairs_.set(i, ph, pl);
}
}
/**
* Attempt to solve a state estimate with the given root-mean-squared delta
* [tolerance].
* @param root0 Root initial guess.
* @param root0.tolerance Root-mean-squared delta tolerance.
* @param root0.maxIter Maximum number of iterations.
* @param root0.printIter Print iterations.
* @returns [BatchLeastSquaresResult] object.
*/
solve({
tolerance = 1e-3,
maxIter = 250,
printIter = false,
}: {
tolerance?: number;
maxIter?: number;
printIter?: boolean;
} = {}): BatchLeastSquaresResult {
let breakFlag = false;
const xNom = BatchLeastSquaresOD.stateToX0_(this.nominal_);
let weightedRms = Infinity;
const atwaMatInit = Matrix.zero(6, 6);
const atwbMatInit = Matrix.zero(6, 1);
let atwaMat = atwaMatInit;
for (let iter = 0; iter < maxIter; iter++) {
atwaMat = atwaMatInit;
let atwbMat = atwbMatInit;
this.propagator_ = this.buildPropagator_(xNom, false);
this.setPropagatorPairs_(xNom);
let rmsTotal = 0.0;
let measCount = 0;
for (const ob of this.observations_) {
const noise = ob.noise;
const aMat = ob.jacobian(this.propPairs_);
const aMatTN = aMat.transpose().multiply(noise);
const bMat = ob.residual(this.propagator_);
atwaMat = atwaMat.add(aMatTN.multiply(aMat));
atwbMat = atwbMat.add(aMatTN.multiply(bMat));
rmsTotal += bMat.transpose().multiply(noise).multiply(bMat).elements[0][0];
measCount += noise.rows;
}
const newWeightedRms = Math.sqrt(rmsTotal / measCount);
if (printIter) {
// eslint-disable-next-line no-console
console.log(`${iter + 1}: rms=${newWeightedRms} x=${new Vector(xNom)}`);
}
if (Math.abs((weightedRms - newWeightedRms) / weightedRms) <= tolerance) {
breakFlag = true;
}
weightedRms = newWeightedRms;
const dX = atwaMat.inverse().multiply(atwbMat);
for (let i = 0; i < 6; i++) {
xNom[i] += dX.elements[i][0];
}
if (breakFlag) {
break;
}
}
const p = atwaMat.inverse();
const covariance = new StateCovariance(p, CovarianceFrame.ECI);
return new BatchLeastSquaresResult(
this.buildPropagator_(xNom, false).propagate(this.start_),
covariance,
weightedRms,
);
}
}