UNPKG

ootk

Version:

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

213 lines (192 loc) 6.12 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 { CostFunction, SimplexEntry } from './SimplexEntry.js'; // / Derivative-free Nelder-Mead simplex optimizer. export class DownhillSimplex { private constructor() { // disable constructor } /** * Compute the centroid from a list of [SimplexEntry] objects, using cost * function [f]. * @param f Cost function * @param xss Simplex entries * @returns The centroid. */ private static _centroid(f: CostFunction, xss: SimplexEntry[]): SimplexEntry { const n = xss[0].points.length; const m = xss.length - 1; const output = new Float64Array(n); for (let i = 0; i < m; i++) { for (let j = 0; j < n; j++) { output[j] += xss[i].points[j]; } } for (let i = 0; i < n; i++) { output[i] /= m; } return new SimplexEntry(f, output); } private static _shrink(s: number, xss: SimplexEntry[]): void { const x1 = xss[0]; for (let i = 1; i < xss.length; i++) { const xi = xss[i]; xss[i] = x1.modify(s, xi, x1); } } /** * Generate a new simplex from initial guess [x0], and an optional * simplex [step] value. * @param x0 Initial guess * @param step Simplex step * @returns The simplex. */ static generateSimplex(x0: Float64Array, step = 0.01): Float64Array[] { const output: Float64Array[] = [x0.slice(0)]; for (let i = 0; i < x0.length; i++) { const tmp = x0.slice(0); tmp[i] += tmp[i] * step; output.push(tmp); } return output; } /** * Perform derivative-free Nelder-Mead simplex optimization to minimize the * cost function [f] for the initial simplex [xs]. * * Optional arguments: * - `xTolerance`: centroid delta termination criteria * - `fTolerance`: cost function delta termination criteria * - `maxIter`: maximum number of optimization iterations * - `adaptive`: use adaptive coefficients if possible * - `printIter`: print a debug statement after each iteration * @param f Cost function * @param xs Initial simplex * @param root0 Root0 * @param root0.xTolerance Root0.xTolerance * @param root0.fTolerance Root0.fTolerance * @param root0.maxIter Root0.maxIter * @param root0.adaptive Root0.adaptive * @param root0.printIter Root0.printIter * @returns The optimal input value. */ static solveSimplex( f: CostFunction, xs: Float64Array[], { xTolerance = 1e-12, fTolerance = 1e-12, maxIter = 10000, adaptive = false, printIter = false, }: { xTolerance?: number; fTolerance?: number; maxIter?: number; adaptive?: boolean; printIter?: boolean; }, ): Float64Array { let a: number; let g: number; let p: number; let s: number; const n = xs.length - 1; if (adaptive && n >= 2) { a = 1.0; g = 1.0 + 2.0 / n; p = 0.75 - 1.0 / (2.0 * n); s = 1.0 - 1.0 / n; } else { a = 1.0; g = 2.0; p = 0.5; s = 0.5; } let iter = 0; let action = 'init'; const ordered: SimplexEntry[] = []; for (const x of xs) { ordered.push(new SimplexEntry(f, x)); } // eslint-disable-next-line no-constant-condition while (true) { ordered.sort((x, y) => x.score - y.score); const x0 = DownhillSimplex._centroid(f, ordered); // update exit criterea let xd = 0.0; let fd = 0.0; for (let i = 1; i < ordered.length; i++) { xd = Math.max(xd, x0.distance(ordered[i])); fd = Math.max(fd, Math.abs(x0.score - ordered[i].score)); } if (printIter) { // eslint-disable-next-line no-console console.log(`${iter}: score=${x0.score} xd=${xd} fd=${fd} [${action}]`); } if (iter !== 0 && (xd < xTolerance || fd < fTolerance)) { return ordered[0].points; } if (iter >= maxIter) { return ordered[0].points; } iter++; // reflection const xr = x0.modify(a, x0, ordered[ordered.length - 1]); if (ordered[0].score <= xr.score && xr.score < ordered[ordered.length - 2].score) { ordered[ordered.length - 1] = xr; action = 'reflect'; continue; } // expansion if (xr.score < ordered[0].score) { const xe = x0.modify(g, xr, x0); if (xe.score < xr.score) { ordered[ordered.length - 1] = xe; } else { ordered[ordered.length - 1] = xr; } action = 'expand'; continue; } // contraction if (xr.score < ordered[ordered.length - 1].score) { const xc = x0.modify(p, xr, x0); if (xc.score < xr.score) { ordered[ordered.length - 1] = xc; action = 'contract'; continue; } else { DownhillSimplex._shrink(s, ordered); action = 'shrink'; continue; } } else if (xr.score >= ordered[ordered.length - 1].score) { const xc = x0.modify(p, ordered[ordered.length - 1], x0); if (xc.score < ordered[ordered.length - 1].score) { ordered[ordered.length - 1] = xc; action = 'contract'; continue; } else { DownhillSimplex._shrink(s, ordered); action = 'shrink'; continue; } } } } }