Intermediate stage

This commit is contained in:
Martin Asprusten 2026-04-02 12:16:14 +02:00
parent eaedfaf29c
commit dd63c64443
No known key found for this signature in database
2 changed files with 141 additions and 132 deletions

View File

@ -1361,36 +1361,6 @@ export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates
let ownShipHeadedInwards = interpolationParameters.secondOwnAltitude < interpolationParameters.firstOwnAltitude; let ownShipHeadedInwards = interpolationParameters.secondOwnAltitude < interpolationParameters.firstOwnAltitude;
let targetHeadedInwards = interpolationParameters.secondTargetAltitude < interpolationParameters.firstTargetAltitude; let targetHeadedInwards = interpolationParameters.secondTargetAltitude < interpolationParameters.firstTargetAltitude;
// Define some helper functions for later
const getRandomPerpendicularVector = (vector: number[][]) => {
let perpendicularX = Math.random();
let perpendicularY = Math.random();
let perpendicularZ = Math.random();
let normalVector: number[][];
if (Math.abs(vector[0][0]) > 0.0001) {
normalVector = [
[(-vector[1][0] * perpendicularY - vector[2][0]*perpendicularZ) / vector[0][0]],
[perpendicularY],
[perpendicularZ]
];
} else if (Math.abs(vector[1][0]) > 0.0001) {
normalVector = [
[perpendicularX],
[(-vector[0][0] / perpendicularX - vector[2][0] / perpendicularZ) / vector[1][0]],
[perpendicularZ]
];
} else {
normalVector = [
[perpendicularX],
[perpendicularY],
[(-vector[0][0] * perpendicularX - vector[1][0] * perpendicularY) / vector[2][0]]
]
};
return normalizeVector(normalVector);
};
const getTrueAnomalyFromAltitude = (altitude: number, semiLatusRectum: number, eccentricity: number, headingInwards: boolean) => { const getTrueAnomalyFromAltitude = (altitude: number, semiLatusRectum: number, eccentricity: number, headingInwards: boolean) => {
let trueAnomaly = Math.acos((semiLatusRectum - altitude) / (altitude * eccentricity)); let trueAnomaly = Math.acos((semiLatusRectum - altitude) / (altitude * eccentricity));
if (headingInwards) { if (headingInwards) {
@ -1400,7 +1370,7 @@ export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates
} }
const getPossibleTargetVectors = (ownOrbit: Orbit, ownAltitude: number, targetAltitude: number, distanceToTarget: number): [number[][], number[][], number[][]] => { const getPossibleTargetVectors = (ownOrbit: Orbit, ownAltitude: number, targetAltitude: number, distanceToTarget: number): [number[][], number[][], number[][], number, number] => {
let ownTrueAnomaly = getTrueAnomalyFromAltitude(ownAltitude, ownOrbit.semiLatusRectum, ownOrbit.eccentricity, ownShipHeadedInwards); let ownTrueAnomaly = getTrueAnomalyFromAltitude(ownAltitude, ownOrbit.semiLatusRectum, ownOrbit.eccentricity, ownShipHeadedInwards);
let ownLocalX = ownAltitude * Math.cos(ownTrueAnomaly); let ownLocalX = ownAltitude * Math.cos(ownTrueAnomaly);
@ -1413,7 +1383,7 @@ export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates
) )
); );
let perpendicularInPlane = normalizeVector(vectorCrossProduct(ownDirection, ownOrbit.coordinateAxes[2])); let perpendicularInPlane = normalizeVector(vectorCrossProduct(ownOrbit.coordinateAxes[2], ownDirection));
let perpendicularOutOfPlane = normalizeVector(vectorCrossProduct(ownDirection, perpendicularInPlane)); let perpendicularOutOfPlane = normalizeVector(vectorCrossProduct(ownDirection, perpendicularInPlane));
// Find phase angle to target // Find phase angle to target
@ -1426,17 +1396,22 @@ export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates
let vectorOne = multiplyMatrixWithScalar(distanceAlongNormal, perpendicularInPlane); let vectorOne = multiplyMatrixWithScalar(distanceAlongNormal, perpendicularInPlane);
let vectorTwo = multiplyMatrixWithScalar(distanceAlongNormal, perpendicularOutOfPlane); let vectorTwo = multiplyMatrixWithScalar(distanceAlongNormal, perpendicularOutOfPlane);
return [root, vectorOne, vectorTwo]; return [root, vectorOne, vectorTwo, distanceAlongDirection, distanceAlongNormal];
} }
const getFunctionAndDerivatives = (possiblePositionOne: [number[][], number[][], number[][]], possiblePositionTwo: [number[][], number[][], number[][]], altitudeOne: number, altitudeTwo: number, anomalyDifference: number): [(angleOne: number, angleTwo: number) => number, (angleOne: number, angleTwo: number) => number, (angleOne: number, angleTwo: number) => number] => { let firstPositionPossibleVectors = getPossibleTargetVectors(ownCoordinates.orbit, interpolationParameters.firstOwnAltitude, interpolationParameters.firstTargetAltitude, interpolationParameters.firstDistance);
let c1 = possiblePositionOne[0]; let secondPositionPossibleVectors = getPossibleTargetVectors(ownCoordinates.orbit, interpolationParameters.secondOwnAltitude, interpolationParameters.secondTargetAltitude, interpolationParameters.secondDistance);
let p11 = possiblePositionOne[1];
let p12 = possiblePositionOne[2];
let c2 = possiblePositionTwo[0]; let targetFirstTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.firstTargetAltitude, targetSemiLatusRectum, targetEccentricity, targetHeadedInwards);
let p21 = possiblePositionTwo[1]; let targetSecondTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.secondTargetAltitude, targetSemiLatusRectum, targetEccentricity, targetHeadedInwards);
let p22 = possiblePositionTwo[2];
let c1 = firstPositionPossibleVectors[0];
let p11 = firstPositionPossibleVectors[1];
let p12 = firstPositionPossibleVectors[2];
let c2 = secondPositionPossibleVectors[0];
let p21 = secondPositionPossibleVectors[1];
let p22 = secondPositionPossibleVectors[2];
// Scale everything to avoid numerical explosions // Scale everything to avoid numerical explosions
let scaleFactor = 1 / getVectorMagnitude(c2); let scaleFactor = 1 / getVectorMagnitude(c2);
@ -1448,10 +1423,10 @@ export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates
p21 = multiplyMatrixWithScalar(scaleFactor, p21); p21 = multiplyMatrixWithScalar(scaleFactor, p21);
p22 = multiplyMatrixWithScalar(scaleFactor, p22); p22 = multiplyMatrixWithScalar(scaleFactor, p22);
let scaledAltitudeOne = altitudeOne * scaleFactor; let scaledAltitudeOne = interpolationParameters.firstTargetAltitude * scaleFactor;
let scaledAltitudeTwo = altitudeTwo * scaleFactor; let scaledAltitudeTwo = interpolationParameters.secondTargetAltitude * scaleFactor;
const constantPart = scaledAltitudeOne * scaledAltitudeTwo * Math.cos(anomalyDifference); const constantPart = scaledAltitudeOne * scaledAltitudeTwo * Math.cos(targetSecondTrueAnomaly - targetFirstTrueAnomaly);
const functionToMinimize = (angleOne: number, angleTwo: number) => { const functionToMinimize = (angleOne: number, angleTwo: number) => {
let d1 = addVector(c1, addVector( let d1 = addVector(c1, addVector(
@ -1495,45 +1470,60 @@ export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates
return vectorDotProduct(d1, d2Dy); return vectorDotProduct(d1, d2Dy);
} }
return [functionToMinimize, partialDerivativeAngleOne, partialDerivativeAngleTwo]; let firstOwnTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.firstOwnAltitude, ownCoordinates.orbit.semiLatusRectum, ownCoordinates.orbit.eccentricity, ownShipHeadedInwards);
let secondOwnTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.secondOwnAltitude, ownCoordinates.orbit.semiLatusRectum, ownCoordinates.orbit.eccentricity, ownShipHeadedInwards);
let horizontalAngleDifference = (secondOwnTrueAnomaly - interpolationParameters.secondPhaseAngle) - (firstOwnTrueAnomaly - interpolationParameters.firstPhaseAngle);
const phaseAngleFunction = (angleOne: number, angleTwo: number): number => {
let d1 = addVector(c1, multiplyMatrixWithScalar(Math.cos(angleOne), p11));
let d2 = addVector(c2, multiplyMatrixWithScalar(Math.cos(angleTwo), p21));
return vectorDotProduct(d1, d2) - Math.cos(horizontalAngleDifference) * getVectorMagnitude(d1) * getVectorMagnitude(d2);
} }
let firstPositionPossibleVectors = getPossibleTargetVectors(ownCoordinates.orbit, interpolationParameters.firstOwnAltitude, interpolationParameters.firstTargetAltitude, interpolationParameters.firstDistance); const phaseAnglePartialDerivativeAngleOne = (angleOne: number, angleTwo: number) => {
let secondPositionPossibleVectors = getPossibleTargetVectors(ownCoordinates.orbit, interpolationParameters.secondOwnAltitude, interpolationParameters.secondTargetAltitude, interpolationParameters.secondDistance); let d1 = addVector(c1, multiplyMatrixWithScalar(Math.cos(angleOne), p11));
let d1Dx = multiplyMatrixWithScalar(-Math.sin(angleOne), p11);
let d2 = addVector(c2, multiplyMatrixWithScalar(Math.cos(angleTwo), p21));
// Since we know a lot about the target orbit, we can figure out how far it is supposed to be between the two points return vectorDotProduct(d1Dx, d2) - Math.cos(horizontalAngleDifference) * getVectorMagnitude(d2) * Math.sin(angleOne) * Math.cos(angleOne) * firstPositionPossibleVectors[4]**2 / getVectorMagnitude(d1);
let firstTargetTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.firstTargetAltitude, targetSemiLatusRectum, targetEccentricity, targetHeadedInwards); }
let secondTargetTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.secondTargetAltitude, targetSemiLatusRectum, targetEccentricity, targetHeadedInwards);
let [minimizeFunction, partialDerivativeAngleOne, partialDerivativeAngleTwo] = getFunctionAndDerivatives(firstPositionPossibleVectors, secondPositionPossibleVectors, interpolationParameters.firstTargetAltitude, interpolationParameters.secondTargetAltitude, secondTargetTrueAnomaly - firstTargetTrueAnomaly);
const gradientDescent = (startingGuessAngleOne: number, startingGuessAngleTwo: number): [number, number, number] => { const gradientDescent = (startingGuessAngleOne: number, startingGuessAngleTwo: number): [number, number, number] => {
let angleOne = startingGuessAngleOne; let angleOne = startingGuessAngleOne;
let angleTwo = startingGuessAngleTwo; let angleTwo = startingGuessAngleTwo;
let distanceAlongDirection = getVectorMagnitude(firstPositionPossibleVectors[0]);
let distanceAlongPerpendicular = getVectorMagnitude(firstPositionPossibleVectors[1]);
let value = minimizeFunction(angleOne, angleTwo); let value = minimizeFunction(angleOne, angleTwo);
// Do up to 100 minimizing steps // Do up to 100 minimizing steps
for (var i = 0; i < 100; i++) { for (var i = 0; i < 100; i++) {
let dfDx = partialDerivativeAngleOne(angleOne, angleTwo); let dfDx = partialDerivativeAngleOne(angleOne, angleTwo);
let dfDy = partialDerivativeAngleTwo(angleOne, angleTwo); let dfDy = partialDerivativeAngleTwo(angleOne, angleTwo);
let scaling = Math.sqrt(dfDx**2 + dfDy**2); let phaseAngleValue = distanceAlongPerpendicular * Math.cos(angleOne) / distanceAlongDirection - Math.tan(interpolationParameters.firstPhaseAngle);
let directionX = -dfDy / scaling; let phaseAngleDerivative = -distanceAlongPerpendicular * Math.sin(angleOne) / distanceAlongDirection;
let directionY = -dfDx / scaling;
let changeAlongDirection = directionX * dfDx + directionY * dfDy; let changeAngleOne = dfDx * phaseAngleValue / (dfDy * phaseAngleDerivative);
let changeAngleTwo = (dfDx * phaseAngleValue - phaseAngleDerivative * value) / (dfDy * phaseAngleDerivative);
let deadEnd = false;
let trialValue = value;
let trialAngleOne = angleOne; let trialAngleOne = angleOne;
let trialAngleTwo = angleTwo; let trialAngleTwo = angleTwo;
let trialValue = value;
let divisor = 1; let divisor = 1;
// We don't want things to blow up
while (Math.abs(changeAngleOne) / 2**divisor > Math.PI / 1000 || Math.abs(changeAngleTwo) / 2**divisor > Math.PI / 1000) {
divisor +=1;
}
let deadEnd = false;
while (Math.abs(trialValue) >= Math.abs(value)) { while (Math.abs(trialValue) >= Math.abs(value)) {
let stepAngleOne = - value * directionX / (changeAlongDirection * 2 ** divisor); trialAngleOne = angleOne - changeAngleOne / (2 ** divisor);
let stepAngleTwo = - value * directionY / (changeAlongDirection * 2 ** divisor); trialAngleTwo = angleTwo - changeAngleTwo / (2 ** divisor);
trialAngleOne = angleOne + stepAngleOne;
trialAngleTwo = angleTwo + stepAngleTwo;
trialValue = minimizeFunction(trialAngleOne, trialAngleTwo); trialValue = minimizeFunction(trialAngleOne, trialAngleTwo);
divisor += 1; divisor += 1;
@ -1555,24 +1545,29 @@ export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates
return [angleOne, angleTwo, value]; return [angleOne, angleTwo, value];
} }
let firstCenterLength = getVectorMagnitude(firstPositionPossibleVectors[0]);
let firstPerpendicularLength = getVectorMagnitude(firstPositionPossibleVectors[1]);
let secondCenterLength = getVectorMagnitude(secondPositionPossibleVectors[0]);
let secondPerpendicularLength = getVectorMagnitude(secondPositionPossibleVectors[1]);
let angleOneGuess = Math.acos(firstCenterLength * Math.tan(interpolationParameters.firstPhaseAngle) / firstPerpendicularLength);
let angleTwoGuess = Math.acos(secondCenterLength * Math.tan(interpolationParameters.secondPhaseAngle) / secondPerpendicularLength);
let bestAngleOne = 0; let bestAngleOne = 0;
let bestAngleTwo = 0; let bestAngleTwo = 0;
let bestValue = null; let bestValue: number | null = null;
// Try to start at a bunch of different positions and do gradient descent from there [angleOneGuess, -angleOneGuess].forEach(angleOne => {
for (var i = 0; i < 500; i++) { [angleTwoGuess, -angleTwoGuess].forEach(angleTwo => {
for (var j = 0; j < 500; j++) { let value = minimizeFunction(angleOne, angleTwo);
let startingAngleOne = i * 2 * Math.PI / 500.0; if (bestValue == null || value < bestValue) {
let startingAngleTwo = j * 2 * Math.PI / 500.0 bestValue = value;
let startingValue = minimizeFunction(startingAngleOne, startingAngleTwo); bestAngleOne = angleOne;
bestAngleTwo = angleTwo;
if (bestValue == null || Math.abs(startingValue) < Math.abs(bestValue)) {
bestAngleOne = startingAngleOne;
bestAngleTwo = startingAngleTwo;
bestValue = startingValue;
}
}
} }
});
});
[bestAngleOne, bestAngleOne, bestValue] = gradientDescent(bestAngleOne, bestAngleTwo); [bestAngleOne, bestAngleOne, bestValue] = gradientDescent(bestAngleOne, bestAngleTwo);

View File

@ -10,9 +10,11 @@ export interface InterpolationParameters {
firstOwnAltitude: number, firstOwnAltitude: number,
firstTargetAltitude: number, firstTargetAltitude: number,
firstDistance: number, firstDistance: number,
firstPhaseAngle: number,
secondOwnAltitude: number, secondOwnAltitude: number,
secondTargetAltitude: number, secondTargetAltitude: number,
secondDistance: number, secondDistance: number,
secondPhaseAngle: number,
} }
export const DefaultInterpolationParameters: InterpolationParameters = { export const DefaultInterpolationParameters: InterpolationParameters = {
@ -24,9 +26,11 @@ export const DefaultInterpolationParameters: InterpolationParameters = {
firstOwnAltitude: 0, firstOwnAltitude: 0,
firstTargetAltitude: 0, firstTargetAltitude: 0,
firstDistance: 0, firstDistance: 0,
firstPhaseAngle: 0,
secondOwnAltitude: 0, secondOwnAltitude: 0,
secondTargetAltitude: 0, secondTargetAltitude: 0,
secondDistance: 0 secondDistance: 0,
secondPhaseAngle: 0
} }
export class InterpolateOrbitGui { export class InterpolateOrbitGui {
@ -81,6 +85,7 @@ export class InterpolateOrbitGui {
let firstOwnAltitudeInput = createInputWithLabel("Own altitude:", 0); let firstOwnAltitudeInput = createInputWithLabel("Own altitude:", 0);
let firstTargetAltitudeInput = createInputWithLabel("Target altitude:", 0); let firstTargetAltitudeInput = createInputWithLabel("Target altitude:", 0);
let firstDistanceInput = createInputWithLabel("Distance to target:", 0); let firstDistanceInput = createInputWithLabel("Distance to target:", 0);
let firstPhaseAngleInput = createInputWithLabel("Phase angle:", -180, 180);
let instantTwoHeader = document.createElement("h4"); let instantTwoHeader = document.createElement("h4");
instantTwoHeader.appendChild(document.createTextNode("Measurement two:")); instantTwoHeader.appendChild(document.createTextNode("Measurement two:"));
@ -89,6 +94,7 @@ export class InterpolateOrbitGui {
let secondOwnAltitudeInput = createInputWithLabel("Own altitude:", 0); let secondOwnAltitudeInput = createInputWithLabel("Own altitude:", 0);
let secondTargetAltitudeInput = createInputWithLabel("Target altitude:", 0); let secondTargetAltitudeInput = createInputWithLabel("Target altitude:", 0);
let secondDistanceInput = createInputWithLabel("Distance to target:", 0); let secondDistanceInput = createInputWithLabel("Distance to target:", 0);
let secondPhaseAngleInput = createInputWithLabel("Phase angle:", -180, 180);
this.sourceId = crypto.randomUUID(); this.sourceId = crypto.randomUUID();
const onChange = () => { const onChange = () => {
@ -99,9 +105,11 @@ export class InterpolateOrbitGui {
let firstTargetAltitude = parseFloat(firstTargetAltitudeInput.value); let firstTargetAltitude = parseFloat(firstTargetAltitudeInput.value);
let firstOwnAltitude = parseFloat(firstOwnAltitudeInput.value); let firstOwnAltitude = parseFloat(firstOwnAltitudeInput.value);
let firstDistance = parseFloat(firstDistanceInput.value); let firstDistance = parseFloat(firstDistanceInput.value);
let firstPhaseAngle = parseFloat(firstPhaseAngleInput.value) * Math.PI / 180.0;
let secondTargetAltitude = parseFloat(secondTargetAltitudeInput.value); let secondTargetAltitude = parseFloat(secondTargetAltitudeInput.value);
let secondOwnAltitude = parseFloat(secondOwnAltitudeInput.value); let secondOwnAltitude = parseFloat(secondOwnAltitudeInput.value);
let secondDistance = parseFloat(secondDistanceInput.value); let secondDistance = parseFloat(secondDistanceInput.value);
let secondPhaseAngle = parseFloat(secondPhaseAngleInput.value) * Math.PI / 180.0;
let orbitChoice: "apoapsis" | "speedAndAltitude" = "apoapsis"; let orbitChoice: "apoapsis" | "speedAndAltitude" = "apoapsis";
if (speedAndAltitudeButton.checked) { if (speedAndAltitudeButton.checked) {
@ -117,9 +125,11 @@ export class InterpolateOrbitGui {
firstTargetAltitude: firstTargetAltitude, firstTargetAltitude: firstTargetAltitude,
firstOwnAltitude: firstOwnAltitude, firstOwnAltitude: firstOwnAltitude,
firstDistance: firstDistance, firstDistance: firstDistance,
firstPhaseAngle: firstPhaseAngle,
secondTargetAltitude: secondTargetAltitude, secondTargetAltitude: secondTargetAltitude,
secondOwnAltitude: secondOwnAltitude, secondOwnAltitude: secondOwnAltitude,
secondDistance: secondDistance, secondDistance: secondDistance,
secondPhaseAngle: secondPhaseAngle
}, this.sourceId) }, this.sourceId)
}; };
@ -133,9 +143,11 @@ export class InterpolateOrbitGui {
firstOwnAltitudeInput, firstOwnAltitudeInput,
firstTargetAltitudeInput, firstTargetAltitudeInput,
firstDistanceInput, firstDistanceInput,
firstPhaseAngleInput,
secondOwnAltitudeInput, secondOwnAltitudeInput,
secondTargetAltitudeInput, secondTargetAltitudeInput,
secondDistanceInput secondDistanceInput,
secondPhaseAngleInput
].forEach(input => input.addEventListener("change", onChange)); ].forEach(input => input.addEventListener("change", onChange));
interpolationParameters.listenToValue((source, value) => { interpolationParameters.listenToValue((source, value) => {
@ -162,10 +174,12 @@ export class InterpolateOrbitGui {
firstOwnAltitudeInput.value = value.firstOwnAltitude.toString(); firstOwnAltitudeInput.value = value.firstOwnAltitude.toString();
firstTargetAltitudeInput.value = value.firstTargetAltitude.toString(); firstTargetAltitudeInput.value = value.firstTargetAltitude.toString();
firstDistanceInput.value = value.firstDistance.toString(); firstDistanceInput.value = value.firstDistance.toString();
firstPhaseAngleInput.value = (value.firstPhaseAngle * 180 / Math.PI).toString();
secondOwnAltitudeInput.value = value.secondOwnAltitude.toString(); secondOwnAltitudeInput.value = value.secondOwnAltitude.toString();
secondTargetAltitudeInput.value = value.secondTargetAltitude.toString(); secondTargetAltitudeInput.value = value.secondTargetAltitude.toString();
secondDistanceInput.value = value.secondDistance.toString(); secondDistanceInput.value = value.secondDistance.toString();
secondPhaseAngleInput.value = (value.secondPhaseAngle * 180 / Math.PI).toString();
}); });
} }
} }