diff --git a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_FlightPlanPage.js b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_FlightPlanPage.js
index 744ec54ac5d..ffb50bfe31d 100644
--- a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_FlightPlanPage.js
+++ b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_FlightPlanPage.js
@@ -146,6 +146,10 @@ class CDUFlightPlanPage {
waypointsAndMarkers.push({ wp, fpIndex: i, inAlternate: false, inMissedApproach, distanceFromLastLine, isActive: isActiveLeg && pseudoWaypointsOnLeg.length === 0 });
+ if (wp.calculated && wp.calculated.endsInTooSteepPath) {
+ waypointsAndMarkers.push({ marker: Markers.TOO_STEEP_PATH, fpIndex: i, inAlternate: false, inMissedApproach });
+ }
+
if (i === targetPlan.destinationLegIndex) {
destinationAirportOffset = Math.max(waypointsAndMarkers.length - 4, 0);
}
@@ -579,7 +583,12 @@ class CDUFlightPlanPage {
scrollWindow[rowI] = waypointsAndMarkers[winI];
addLskAt(rowI, 0, (value, scratchpadCallback) => {
if (value === FMCMainDisplay.clrValue) {
- CDUFlightPlanPage.clearElement(mcdu, fpIndex, offset, forPlan, inAlternate, scratchpadCallback);
+ if (marker.marker === Markers.FPLN_DISCONTINUITY) {
+ CDUFlightPlanPage.clearElement(mcdu, fpIndex, offset, forPlan, inAlternate, scratchpadCallback);
+ } else {
+ mcdu.setScratchpadMessage(NXSystemMessages.notAllowed);
+ scratchpadCallback();
+ }
return;
} else if (value === "") {
return;
diff --git a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_VerticalRevisionPage.js b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_VerticalRevisionPage.js
index 9c0ea557b9a..014361f72fc 100644
--- a/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_VerticalRevisionPage.js
+++ b/fbw-a32nx/src/base/flybywire-aircraft-a320-neo/html_ui/Pages/VCockpit/Instruments/Airliners/FlyByWire_A320_Neo/CDU/A320_Neo_CDU_VerticalRevisionPage.js
@@ -9,6 +9,8 @@ const WaypointConstraintType = Object.freeze({
DES: 2,
});
+const TOO_STEEP_PATH_BEYOND_MESSAGE = "{amber}TOO STEEP PATH BEYOND{end}";
+
class CDUVerticalRevisionPage {
/**
* @param mcdu
@@ -142,10 +144,12 @@ class CDUVerticalRevisionPage {
}
}
+ const tooSteepPathInfo = waypoint.calculated.endsInTooSteepPath ? TOO_STEEP_PATH_BEYOND_MESSAGE : "";
+
mcdu.setTemplate([
["VERT REV {small}AT{end}{green} " + waypointIdent + "{end}"],
[],
- [""],
+ ["", "", tooSteepPathInfo],
[speedLimitTitle, ""],
[speedLimitCell, "RTA>[color]inop"],
[l3Title, r3Title],
diff --git a/fbw-a32nx/src/systems/fmgc/src/components/fms-messages/FmsMessages.ts b/fbw-a32nx/src/systems/fmgc/src/components/fms-messages/FmsMessages.ts
index 4c8941af198..6ce538fa2ff 100644
--- a/fbw-a32nx/src/systems/fmgc/src/components/fms-messages/FmsMessages.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/components/fms-messages/FmsMessages.ts
@@ -22,6 +22,7 @@ import { FmgcComponent } from '../FmgcComponent';
import { GpsPrimary } from './GpsPrimary';
import { GpsPrimaryLost } from './GpsPrimaryLost';
import { MapPartlyDisplayedLeft, MapPartlyDisplayedRight } from './MapPartlyDisplayed';
+import { TooSteepPathAhead } from '@fmgc/components/fms-messages/TooSteepPathAhead';
/**
* This class manages Type II messages sent from the FMGC.
@@ -63,6 +64,7 @@ export class FmsMessages implements FmgcComponent {
new TdReached(),
new StepAhead(),
new StepDeleted(),
+ new TooSteepPathAhead(),
];
init(baseInstrument: BaseInstrument, flightPlanService: FlightPlanService): void {
diff --git a/fbw-a32nx/src/systems/fmgc/src/components/fms-messages/TooSteepPathAhead.ts b/fbw-a32nx/src/systems/fmgc/src/components/fms-messages/TooSteepPathAhead.ts
new file mode 100644
index 00000000000..86d68c161ad
--- /dev/null
+++ b/fbw-a32nx/src/systems/fmgc/src/components/fms-messages/TooSteepPathAhead.ts
@@ -0,0 +1,32 @@
+// Copyright (c) 2021-2023 FlyByWire Simulations
+//
+// SPDX-License-Identifier: GPL-3.0
+
+import { GuidanceController } from '@fmgc/guidance/GuidanceController';
+import { FMMessageTypes } from '@flybywiresim/fbw-sdk';
+
+import { FMMessageSelector, FMMessageUpdate } from './FmsMessages';
+
+export class TooSteepPathAhead implements FMMessageSelector {
+ message = FMMessageTypes.TooSteepPathAhead;
+
+ private guidanceController: GuidanceController;
+
+ private lastState = false;
+
+ init(baseInstrument: BaseInstrument): void {
+ this.guidanceController = baseInstrument.guidanceController;
+ }
+
+ process(_: number): FMMessageUpdate {
+ const newState = this.guidanceController.vnavDriver.shouldShowTooSteepPathAhead();
+
+ if (newState !== this.lastState) {
+ this.lastState = newState;
+
+ return newState ? FMMessageUpdate.SEND : FMMessageUpdate.RECALL;
+ }
+
+ return FMMessageUpdate.NO_ACTION;
+ }
+}
diff --git a/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/FlightPlanLeg.ts b/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/FlightPlanLeg.ts
index a6e46ed99aa..e8246d42810 100644
--- a/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/FlightPlanLeg.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/flightplanning/legs/FlightPlanLeg.ts
@@ -63,6 +63,8 @@ export interface LegCalculations {
cumulativeDistanceWithTransitions: number;
/** The cumulative distance in nautical miles from this point to the missed approach point, with leg transition turns taken into account. */
cumulativeDistanceToEndWithTransitions: number;
+ /** Whether the leg terminates in a vertical discontinuity */
+ endsInTooSteepPath: boolean;
}
/**
diff --git a/fbw-a32nx/src/systems/fmgc/src/flightplanning/plans/FlightPlan.ts b/fbw-a32nx/src/systems/fmgc/src/flightplanning/plans/FlightPlan.ts
index 2bfe23e6fc8..75458a34eb9 100644
--- a/fbw-a32nx/src/systems/fmgc/src/flightplanning/plans/FlightPlan.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/flightplanning/plans/FlightPlan.ts
@@ -626,4 +626,23 @@ export class FlightPlan
150 && isCruisePhase) || isDesOrApprPhase;
+
+ if (!isManagedLateralMode || !isCloseToDestination) {
+ return false;
+ }
+
+ return this.flightPlanService.active?.hasTooSteepPathAhead();
+ }
}
class FcuModeObserver {
@@ -754,7 +769,9 @@ class FcuModeObserver {
LateralMode.NAV,
LateralMode.LOC_CPT,
LateralMode.LOC_TRACK,
+ LateralMode.LAND,
LateralMode.RWY,
+ LateralMode.GA_TRACK,
];
private VERT_CLIMB_MODES: VerticalMode[] = [
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/VnavDriver.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/VnavDriver.ts
index 52d93f416a7..282b6755022 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/VnavDriver.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/VnavDriver.ts
@@ -32,6 +32,7 @@ import {
} from './profile/NavGeometryProfile';
import { VMLeg } from '@fmgc/guidance/lnav/legs/VM';
import { FMLeg } from '@fmgc/guidance/lnav/legs/FM';
+import { MathUtils } from '@flybywiresim/fbw-sdk';
export class VnavDriver implements GuidanceComponent {
version: number = 0;
@@ -288,11 +289,10 @@ export class VnavDriver implements GuidanceComponent {
const isPastCstrDeceleration =
checkpoint.reason === VerticalCheckpointReason.StartDecelerationToConstraint &&
- currentDistanceFromStart - checkpoint.distanceFromStart > -1e-4;
+ MathUtils.isCloseToGreaterThan(currentDistanceFromStart, checkpoint.distanceFromStart);
const isPastLimitDeceleration =
checkpoint.reason === VerticalCheckpointReason.StartDecelerationToLimit &&
- currentAltitude - checkpoint.altitude < 1e-4;
-
+ MathUtils.isCloseToLessThan(currentAltitude, checkpoint.altitude);
if (
isSpeedChangePoint(checkpoint) &&
checkpoint.targetSpeed >= decelPointSpeed &&
@@ -630,6 +630,10 @@ export class VnavDriver implements GuidanceComponent {
? completeLegAlongTrackPathDtg + referenceLeg.calculated.cumulativeDistanceToEndWithTransitions
: undefined;
}
+
+ shouldShowTooSteepPathAhead(): boolean {
+ return this.profileManager.shouldShowTooSteepPathAhead();
+ }
}
/// To check whether the value changed from old to new, but not if both values are NaN. (NaN !== NaN in JS)
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/climb/ClimbStrategy.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/climb/ClimbStrategy.ts
index a1477dcde74..b1808d573d6 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/climb/ClimbStrategy.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/climb/ClimbStrategy.ts
@@ -3,9 +3,12 @@
// SPDX-License-Identifier: GPL-3.0
import { VerticalProfileComputationParametersObserver } from '@fmgc/guidance/vnav/VerticalProfileComputationParameters';
-import { DEFAULT_AIRCRAFT_CONTROL_SURFACE_CONFIG, DescentStrategy } from '@fmgc/guidance/vnav/descent/DescentStrategy';
+import { DescentStrategy } from '@fmgc/guidance/vnav/descent/DescentStrategy';
import { WindComponent } from '@fmgc/guidance/vnav/wind';
-import { AircraftConfiguration as AircraftCtlSurfcConfiguration } from '@fmgc/guidance/vnav/descent/ApproachPathBuilder';
+import {
+ AircraftConfiguration as AircraftCtlSurfcConfiguration,
+ DEFAULT_AIRCRAFT_CONTROL_SURFACE_CONFIG,
+} from '@fmgc/guidance/vnav/descent/ApproachPathBuilder';
import { MathUtils } from '@flybywiresim/fbw-sdk';
import { UnitType } from '@microsoft/msfs-sdk';
import { AircraftConfig, EngineModelParameters } from '@fmgc/flightplanning/AircraftConfigTypes';
@@ -341,7 +344,7 @@ export class ClimbThrustClimbStrategy implements ClimbStrategy {
tropoPause,
config.speedbrakesExtended,
config.flapConfig,
- config.speedbrakesExtended,
+ config.gearExtended,
perfFactor,
);
}
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/ApproachPathBuilder.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/ApproachPathBuilder.ts
index b07cf5dfcef..9db6351749b 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/ApproachPathBuilder.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/ApproachPathBuilder.ts
@@ -100,13 +100,43 @@ export interface AircraftConfiguration {
gearExtended: boolean;
}
-export class AircraftConfigurationProfile {
- static getBySpeed(speed: Knots, parameters: VerticalProfileComputationParameters): AircraftConfiguration {
- return {
- flapConfig: FlapConfigurationProfile.getBySpeed(speed, parameters),
- speedbrakesExtended: false,
- gearExtended: speed < parameters.flapRetractionSpeed,
- };
+export const DEFAULT_AIRCRAFT_CONTROL_SURFACE_CONFIG: Readonly = {
+ flapConfig: FlapConf.CLEAN,
+ speedbrakesExtended: false,
+ gearExtended: false,
+};
+
+/**
+ * Use this class to generate `AircraftConfiguration` objects. This reuses the same object to avoid unnecessary allocations.
+ * If multiple configurations are generated from the same register, they will be the same object. Use multiple registers
+ * if you need different configurations at the same time.
+ *
+ * @example
+ * const parameters = new VerticalProfileComputationParametersObserver().get();
+ * const register = new AircraftConfigurationRegister();
+ *
+ * // Appropriate configuration to fly at 250 kts
+ * const config = register.setFromSpeed(250, parameters);
+ *
+ * // Appropriate configuration to fly at 200 kts
+ * const config2 = register.setFromSpeed(200, parameters);
+ *
+ * // These configurations are now the *same*
+ * assert(config === config2);
+ */
+export class AircraftConfigurationRegister {
+ private readonly config: AircraftConfiguration = { ...DEFAULT_AIRCRAFT_CONTROL_SURFACE_CONFIG };
+
+ setFromSpeed(speed: Knots, parameters: VerticalProfileComputationParameters, useSpeedbrakes = false) {
+ this.config.flapConfig = FlapConfigurationProfile.getBySpeed(speed, parameters);
+ this.config.speedbrakesExtended = useSpeedbrakes;
+ this.config.gearExtended = speed < parameters.flapRetractionSpeed;
+
+ return this.config;
+ }
+
+ get(): Readonly {
+ return this.config;
}
}
@@ -115,6 +145,10 @@ export class ApproachPathBuilder {
private fpaStrategy: FlightPathAngleStrategy;
+ private readonly configuration = new AircraftConfigurationRegister();
+
+ private static readonly DISTANCE_EPSILON = 1e-4;
+
constructor(
private observer: VerticalProfileComputationParametersObserver,
atmosphericConditions: AtmosphericConditions,
@@ -161,7 +195,7 @@ export class ApproachPathBuilder {
managedDescentSpeedMach,
estimatedFuelOnBoardAtDestination,
windProfile.getHeadwindComponent(profile.getDistanceFromStart(0), finalAltitude),
- AircraftConfigurationProfile.getBySpeed(approachSpeed, this.observer.get()),
+ this.configuration.setFromSpeed(approachSpeed, this.observer.get()),
);
sequence.addCheckpointFromStep(finalApproachStep, VerticalCheckpointReason.AtmosphericConditions);
@@ -195,7 +229,7 @@ export class ApproachPathBuilder {
}
const speedTarget = speedProfile.getTarget(
- sequence.lastCheckpoint.distanceFromStart - 1e-4,
+ sequence.lastCheckpoint.distanceFromStart - ApproachPathBuilder.DISTANCE_EPSILON,
sequence.lastCheckpoint.altitude,
ManagedSpeedType.Descent,
);
@@ -255,7 +289,10 @@ export class ApproachPathBuilder {
let secondDecelerationSequence: TemporaryCheckpointSequence = null;
// `decelerationSegmentDistance` should be positive
- const tryDecelDistance = (decelerationSegmentDistance: NauticalMiles): NauticalMiles => {
+ const tryDecelDistance = (
+ decelerationSegmentDistance: NauticalMiles,
+ useSpeedbrakes: boolean = false,
+ ): NauticalMiles => {
const currentDecelerationAttempt = new TemporaryCheckpointSequence(sequence.lastCheckpoint);
decelerationSequence = this.buildDecelerationPath(
@@ -263,6 +300,7 @@ export class ApproachPathBuilder {
speedProfile,
windProfile,
distanceFromStart - decelerationSegmentDistance,
+ useSpeedbrakes,
);
currentDecelerationAttempt.push(...decelerationSequence.get());
@@ -273,7 +311,7 @@ export class ApproachPathBuilder {
managedDescentSpeedMach,
decelerationSequence.lastCheckpoint.remainingFuelOnBoard,
windProfile.getHeadwindComponent(distanceFromStart - decelerationSegmentDistance, minimumAltitude),
- AircraftConfigurationProfile.getBySpeed(decelerationSequence.lastCheckpoint.speed, this.observer.get()),
+ this.configuration.setFromSpeed(decelerationSequence.lastCheckpoint.speed, this.observer.get(), useSpeedbrakes),
);
currentDecelerationAttempt.addCheckpointFromStep(descentSegment, VerticalCheckpointReason.AltitudeConstraint);
@@ -292,6 +330,7 @@ export class ApproachPathBuilder {
speedProfile,
windProfile,
constraint.distanceFromStart,
+ useSpeedbrakes,
);
return distanceTraveled - desiredDistanceToCover;
@@ -305,6 +344,43 @@ export class ApproachPathBuilder {
);
tryDecelDistance(solution);
+ // Total distance is deceleration distance (`solution`) + descent distance (`-descentSegment.distanceTraveled`)
+ const distanceTraveled = solution + -descentSegment.distanceTraveled;
+ const finalError = distanceTraveled - desiredDistanceToCover;
+
+ // Even without decelerating and only descending, we cannot make the constraint
+ if (MathUtils.isCloseToNegative(solution) && !MathUtils.isCloseToNegative(finalError)) {
+ // Try with speedbrakes
+ const solutionWithSpeedbrakes = BisectionMethod.findZero(
+ (distance) => tryDecelDistance(distance, true),
+ [0, desiredDistanceToCover],
+ [-0.1, 0.1],
+ NonTerminationStrategy.NegativeErrorResult,
+ );
+ tryDecelDistance(solutionWithSpeedbrakes, true);
+
+ const distanceTraveledWithSpeedbrakes = solutionWithSpeedbrakes + -descentSegment.distanceTraveled;
+ const finalErrorWithSpeedbrakes = distanceTraveledWithSpeedbrakes - desiredDistanceToCover;
+
+ if (
+ MathUtils.isCloseToNegative(solutionWithSpeedbrakes) &&
+ !MathUtils.isCloseToNegative(finalErrorWithSpeedbrakes)
+ ) {
+ // Insert TOO STEEP PATH
+ const scaling = desiredDistanceToCover / -descentSegment.distanceTraveled;
+ this.scaleStepBasedOnLastCheckpoint(sequence.lastCheckpoint, descentSegment, scaling);
+ sequence.addCheckpointFromStep(descentSegment, VerticalCheckpointReason.GeometricPathTooSteep);
+ sequence.copyLastCheckpoint({
+ reason: VerticalCheckpointReason.AltitudeConstraint,
+ altitude: minimumAltitude,
+ });
+
+ constraint.leg.calculated.endsInTooSteepPath = true;
+
+ return;
+ }
+ }
+
sequence.push(...decelerationSequence.get());
const speedTarget = speedProfile.getTarget(
@@ -337,6 +413,7 @@ export class ApproachPathBuilder {
* @param speedProfile
* @param windProfile
* @param targetDistanceFromStart
+ * @param useSpeedbrakes
* @returns
*/
private buildDecelerationPath(
@@ -344,6 +421,7 @@ export class ApproachPathBuilder {
speedProfile: SpeedProfile,
windProfile: HeadwindProfile,
targetDistanceFromStart: NauticalMiles,
+ useSpeedbrakes = false,
): TemporaryCheckpointSequence {
const decelerationSequence = new TemporaryCheckpointSequence(lastCheckpoint);
@@ -353,12 +431,14 @@ export class ApproachPathBuilder {
const isDoneDeclerating = () =>
decelerationSequence.lastCheckpoint.reason === VerticalCheckpointReason.Decel ||
- decelerationSequence.lastCheckpoint.distanceFromStart - targetDistanceFromStart <= 1e-4; // We really only want to prevent floating point errors here
+ MathUtils.isCloseToLessThan(decelerationSequence.lastCheckpoint.distanceFromStart, targetDistanceFromStart); // We really only want to prevent floating point errors here
for (let i = 0; i < 10 && !isDoneDeclerating(); i++) {
const { distanceFromStart, altitude, speed, remainingFuelOnBoard } = decelerationSequence.lastCheckpoint;
- const speedConstraint = speedProfile.getMaxDescentSpeedConstraint(distanceFromStart - 1e-4);
+ const speedConstraint = speedProfile.getMaxDescentSpeedConstraint(
+ distanceFromStart - ApproachPathBuilder.DISTANCE_EPSILON,
+ );
const flapTargetSpeed = FlapConfigurationProfile.findNextExtensionSpeed(speed, parameters);
// This is the managed descent speed, or the speed limit speed.
@@ -389,7 +469,7 @@ export class ApproachPathBuilder {
parameters.managedDescentSpeedMach,
remainingFuelOnBoard,
windProfile.getHeadwindComponent(distanceFromStart, altitude),
- AircraftConfigurationProfile.getBySpeed(speed, parameters),
+ this.configuration.setFromSpeed(speed, parameters, useSpeedbrakes),
);
if (decelerationStep.error !== undefined || decelerationStep.distanceTraveled > 0) {
@@ -404,7 +484,7 @@ export class ApproachPathBuilder {
lastAccelerationCheckpointIndex = decelerationSequence.length;
- if (decelerationStep.distanceTraveled < 1e-4) {
+ if (MathUtils.isCloseToNegative(decelerationStep.distanceTraveled)) {
// We tried to declerate, but it took us beyond targetDistanceFromStart, so we scale down the step
const scaling = Math.min(1, remainingDistance / decelerationStep.distanceTraveled);
this.scaleStepBasedOnLastCheckpoint(decelerationSequence.lastCheckpoint, decelerationStep, scaling);
@@ -430,7 +510,7 @@ export class ApproachPathBuilder {
parameters.managedDescentSpeedMach,
remainingFuelOnBoard - decelerationStep.fuelBurned,
windProfile.getHeadwindComponent(distanceFromStart, altitude),
- AircraftConfigurationProfile.getBySpeed(speedConstraint.maxSpeed, parameters),
+ this.configuration.setFromSpeed(speedConstraint.maxSpeed, parameters, useSpeedbrakes),
);
if (constantStep.distanceTraveled > 0) {
@@ -447,7 +527,7 @@ export class ApproachPathBuilder {
// We don't care about any speed constraint limitations here, because that's what the if block above is for.
const targetSpeed = Math.min(flapTargetSpeed, limitingSpeed);
- const config = AircraftConfigurationProfile.getBySpeed(speed, parameters);
+ const config = this.configuration.setFromSpeed(speed, parameters, useSpeedbrakes);
const decelerationStep = this.fpaStrategy.predictToSpeed(
altitude,
@@ -488,7 +568,11 @@ export class ApproachPathBuilder {
}
if (!isDoneDeclerating()) {
- const config = AircraftConfigurationProfile.getBySpeed(decelerationSequence.lastCheckpoint.speed, parameters);
+ const config = this.configuration.setFromSpeed(
+ decelerationSequence.lastCheckpoint.speed,
+ parameters,
+ useSpeedbrakes,
+ );
// Fly constant speed instead
const constantSpeedStep = this.fpaStrategy.predictToDistance(
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/DescentPathBuilder.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/DescentPathBuilder.ts
index 2dd27106046..206eea56754 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/DescentPathBuilder.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/DescentPathBuilder.ts
@@ -2,45 +2,36 @@
//
// SPDX-License-Identifier: GPL-3.0
-import {
- DescentAltitudeConstraint,
- MaxSpeedConstraint,
- VerticalCheckpoint,
- VerticalCheckpointReason,
-} from '@fmgc/guidance/vnav/profile/NavGeometryProfile';
+import { VerticalCheckpoint, VerticalCheckpointReason } from '@fmgc/guidance/vnav/profile/NavGeometryProfile';
import { BaseGeometryProfile } from '@fmgc/guidance/vnav/profile/BaseGeometryProfile';
-import { ManagedSpeedType, SpeedProfile } from '@fmgc/guidance/vnav/climb/SpeedProfile';
+import { SpeedProfile } from '@fmgc/guidance/vnav/climb/SpeedProfile';
import { AtmosphericConditions } from '@fmgc/guidance/vnav/AtmosphericConditions';
import { VerticalProfileComputationParametersObserver } from '@fmgc/guidance/vnav/VerticalProfileComputationParameters';
import { GeometricPathBuilder } from '@fmgc/guidance/vnav/descent/GeometricPathBuilder';
-import { DescentStrategy, IdleDescentStrategy } from '@fmgc/guidance/vnav/descent/DescentStrategy';
-import { StepResults } from '@fmgc/guidance/vnav/Predictions';
import { HeadwindProfile } from '@fmgc/guidance/vnav/wind/HeadwindProfile';
import { TemporaryCheckpointSequence } from '@fmgc/guidance/vnav/profile/TemporaryCheckpointSequence';
import { AltitudeDescriptor, MathUtils } from '@flybywiresim/fbw-sdk';
-import { SpeedLimit } from '@fmgc/guidance/vnav/SpeedLimit';
import { ConstraintUtils, AltitudeConstraint } from '@fmgc/flightplanning/data/constraint';
import { AircraftConfig } from '@fmgc/flightplanning/AircraftConfigTypes';
+import { IdlePathBuilder } from '@fmgc/guidance/vnav/descent/IdlePathBuilder';
+import { GeometricPathPlanner, PlannedGeometricSegment } from '@fmgc/guidance/vnav/descent/GeometricPathPlanner';
export class DescentPathBuilder {
private geometricPathBuilder: GeometricPathBuilder;
- private idleDescentStrategy: DescentStrategy;
+ private idlePathBuilder: IdlePathBuilder;
constructor(
- private computationParametersObserver: VerticalProfileComputationParametersObserver,
- private atmosphericConditions: AtmosphericConditions,
+ computationParametersObserver: VerticalProfileComputationParametersObserver,
+ atmosphericConditions: AtmosphericConditions,
private readonly acConfig: AircraftConfig,
) {
- this.geometricPathBuilder = new GeometricPathBuilder(
- computationParametersObserver,
- atmosphericConditions,
- this.acConfig,
- );
+ this.idlePathBuilder = new IdlePathBuilder(computationParametersObserver, atmosphericConditions, this.acConfig);
- this.idleDescentStrategy = new IdleDescentStrategy(
+ this.geometricPathBuilder = new GeometricPathBuilder(
computationParametersObserver,
atmosphericConditions,
+ this,
this.acConfig,
);
}
@@ -51,6 +42,7 @@ export class DescentPathBuilder {
speedProfile: SpeedProfile,
windProfile: HeadwindProfile,
cruiseAltitude: Feet,
+ toDistance: NauticalMiles = -Infinity,
) {
const TOL = 10;
const decelPoint: VerticalCheckpoint = { ...sequence.lastCheckpoint };
@@ -58,12 +50,22 @@ export class DescentPathBuilder {
const geometricSequence = new TemporaryCheckpointSequence(decelPoint);
const idleSequence = new TemporaryCheckpointSequence(decelPoint);
- this.buildIdlePath(idleSequence, profile, speedProfile, windProfile, cruiseAltitude);
+ this.idlePathBuilder.buildIdlePath(
+ idleSequence,
+ profile.descentSpeedConstraints,
+ speedProfile,
+ windProfile,
+ cruiseAltitude,
+ toDistance,
+ );
for (let i = profile.descentAltitudeConstraints.length - 1; i >= 0; i -= 1) {
const constraintPoint = profile.descentAltitudeConstraints[i];
- if (constraintPoint.distanceFromStart >= decelPoint.distanceFromStart) {
+ if (
+ constraintPoint.distanceFromStart >= decelPoint.distanceFromStart ||
+ constraintPoint.distanceFromStart <= toDistance
+ ) {
// If we've found a constraint that's beyond the decel point, we can ignore it.
continue;
} else if (!this.isConstraintBelowCruisingAltitude(constraintPoint.constraint, cruiseAltitude)) {
@@ -81,6 +83,7 @@ export class DescentPathBuilder {
const geometricPathPoint = {
distanceFromStart: constraintPoint.distanceFromStart,
altitude: altitudeToContinueFrom,
+ leg: constraintPoint.leg,
};
// Plan geometric path between decel point and geometric path point (point between geometric and idle path)
@@ -98,12 +101,20 @@ export class DescentPathBuilder {
this.geometricPathBuilder.executeGeometricSegments(
geometricSequence,
geometricSegments,
- profile.descentSpeedConstraints.slice(),
+ profile,
+ speedProfile,
windProfile,
);
idleSequence.reset(geometricSequence.lastCheckpoint);
- this.buildIdlePath(idleSequence, profile, speedProfile, windProfile, cruiseAltitude);
+ this.idlePathBuilder.buildIdlePath(
+ idleSequence,
+ profile.descentSpeedConstraints,
+ speedProfile,
+ windProfile,
+ cruiseAltitude,
+ toDistance,
+ );
}
}
@@ -117,233 +128,16 @@ export class DescentPathBuilder {
sequence.push(...idleSequence.get());
}
- private buildIdlePath(
- sequence: TemporaryCheckpointSequence,
- profile: BaseGeometryProfile,
- speedProfile: SpeedProfile,
- windProfile: HeadwindProfile,
- topOfDescentAltitude: Feet,
- ): void {
- // Assume the last checkpoint is the start of the geometric path
- sequence.copyLastCheckpoint({ reason: VerticalCheckpointReason.IdlePathEnd });
-
- const { managedDescentSpeedMach, descentSpeedLimit } = this.computationParametersObserver.get();
- const speedConstraintsAhead = this.speedConstraintGenerator(profile.descentSpeedConstraints, sequence);
-
- // We try to figure out what speed we might be decelerating for
- let previousCasTarget =
- this.tryGetAnticipatedTarget(
- sequence,
- profile.descentSpeedConstraints,
- speedProfile.shouldTakeDescentSpeedLimitIntoAccount() ? descentSpeedLimit : null,
- ) ??
- speedProfile.getTarget(
- sequence.lastCheckpoint.distanceFromStart,
- sequence.lastCheckpoint.altitude,
- ManagedSpeedType.Descent,
- );
- let wasPreviouslyUnderSpeedLimitAltitude =
- speedProfile.shouldTakeDescentSpeedLimitIntoAccount() &&
- sequence.lastCheckpoint.altitude < descentSpeedLimit.underAltitude;
-
- for (let i = 0; i < 100 && topOfDescentAltitude - sequence.lastCheckpoint.altitude > 1; i++) {
- const { distanceFromStart, altitude, speed, remainingFuelOnBoard } = sequence.lastCheckpoint;
- const headwind = windProfile.getHeadwindComponent(distanceFromStart, altitude);
- const isUnderSpeedLimitAltitude =
- speedProfile.shouldTakeDescentSpeedLimitIntoAccount() && altitude < descentSpeedLimit.underAltitude;
-
- const casTarget = speedProfile.getTarget(distanceFromStart - 1e-4, altitude + 1e-4, ManagedSpeedType.Descent);
- const currentSpeedTarget = Math.min(
- casTarget,
- this.atmosphericConditions.computeCasFromMach(managedDescentSpeedMach, altitude),
- );
- const canAccelerate = currentSpeedTarget > speed;
-
- if (canAccelerate) {
- // Build acceleration path
- const speedStep = this.idleDescentStrategy.predictToSpeed(
- altitude,
- casTarget,
- speed,
- managedDescentSpeedMach,
- remainingFuelOnBoard,
- headwind,
- );
- const scaling = Math.min(1, (topOfDescentAltitude - altitude) / (speedStep.finalAltitude - altitude));
- this.scaleStepBasedOnLastCheckpoint(sequence.lastCheckpoint, speedStep, scaling);
-
- const didCrossoverSpeedLimitAltitude = wasPreviouslyUnderSpeedLimitAltitude && !isUnderSpeedLimitAltitude;
- const checkpointReason = didCrossoverSpeedLimitAltitude
- ? VerticalCheckpointReason.StartDecelerationToLimit
- : VerticalCheckpointReason.StartDecelerationToConstraint;
-
- sequence.addDecelerationCheckpointFromStep(speedStep, checkpointReason, previousCasTarget);
- } else {
- // Try alt path
- let finalAltitude = Math.min(altitude + 1500, topOfDescentAltitude);
- let reason = VerticalCheckpointReason.IdlePathAtmosphericConditions;
- if (isUnderSpeedLimitAltitude && finalAltitude >= descentSpeedLimit.underAltitude) {
- finalAltitude = descentSpeedLimit.underAltitude;
- reason = VerticalCheckpointReason.CrossingDescentSpeedLimit;
- }
-
- const altitudeStep = this.idleDescentStrategy.predictToAltitude(
- altitude,
- finalAltitude,
- speed,
- managedDescentSpeedMach,
- remainingFuelOnBoard,
- headwind,
- );
- // Check if constraint violated
- const nextSpeedConstraint = speedConstraintsAhead.next();
- if (
- !nextSpeedConstraint.done &&
- distanceFromStart + altitudeStep.distanceTraveled < nextSpeedConstraint.value.distanceFromStart
- ) {
- // Constraint violated
- const distanceToConstraint = nextSpeedConstraint.value.distanceFromStart - distanceFromStart;
- const distanceStep = this.idleDescentStrategy.predictToDistance(
- altitude,
- distanceToConstraint,
- speed,
- managedDescentSpeedMach,
- remainingFuelOnBoard,
- headwind,
- );
- sequence.addCheckpointFromStep(distanceStep, VerticalCheckpointReason.SpeedConstraint);
- } else {
- sequence.addCheckpointFromStep(altitudeStep, reason);
- }
- }
-
- previousCasTarget = casTarget;
- wasPreviouslyUnderSpeedLimitAltitude = isUnderSpeedLimitAltitude;
- }
-
- if (sequence.lastCheckpoint.reason === VerticalCheckpointReason.IdlePathAtmosphericConditions) {
- sequence.lastCheckpoint.reason = VerticalCheckpointReason.TopOfDescent;
- } else {
- sequence.copyLastCheckpoint({ reason: VerticalCheckpointReason.TopOfDescent });
- }
- }
-
- // TODO: I really don't know if this function does what it's supposed to, so I hope I don't have to return to it.
- // The problem it's trying to solve is this: After having built the geometric path to the first violating altitude constraint, we might not be at econ speed yet.
- // So then we need to accelerate to it on the idle path. However, we want to figure out what the reason for this acceleration is, i.e whether it is due
- // to the speed limit or a constraint. We need to know this to place the correct checkpoint reason.
- // What the function does is to try and figure this out based on different criteria.
- private tryGetAnticipatedTarget(
- sequence: TemporaryCheckpointSequence,
- speedConstraints: MaxSpeedConstraint[],
- speedLimit?: SpeedLimit,
- ): Knots | null {
- const {
- distanceFromStart: pposDistanceFromStart,
- speed: currentSpeed,
- altitude: currentAltitude,
- } = sequence.lastCheckpoint;
-
- // Find next constraint
- const nextSpeedConstraint = speedConstraints.find(
- (c) => c.distanceFromStart >= pposDistanceFromStart && c.maxSpeed <= currentSpeed,
- );
-
- const isSpeedLimitValidCandidate =
- speedLimit && currentAltitude > speedLimit.underAltitude && currentSpeed > speedLimit.speed;
-
- if (nextSpeedConstraint) {
- // Try to figure out which speed is more important
- if (isSpeedLimitValidCandidate) {
- const altAtConstraint = sequence.interpolateAltitudeBackwards(nextSpeedConstraint.distanceFromStart);
-
- if (speedLimit.underAltitude > altAtConstraint) {
- return speedLimit.speed;
- }
- }
-
- return nextSpeedConstraint.maxSpeed;
- }
-
- // If we did not find a valid speed constraint candidate, see if the speed limit might be a candidate. If so, return it.
- if (isSpeedLimitValidCandidate) {
- return speedLimit.speed;
- }
-
- return null;
- }
-
- private scaleStepBasedOnLastCheckpoint(lastCheckpoint: VerticalCheckpoint, step: StepResults, scaling: number) {
- step.distanceTraveled *= scaling;
- step.fuelBurned *= scaling;
- step.timeElapsed *= scaling;
- step.finalAltitude = (1 - scaling) * lastCheckpoint.altitude + scaling * step.finalAltitude;
- step.speed = (1 - scaling) * lastCheckpoint.speed + scaling * step.speed;
- }
-
- isConstraintBelowCruisingAltitude(constraint: AltitudeConstraint, finalCruiseAltitude: Feet): boolean {
+ private isConstraintBelowCruisingAltitude(constraint: AltitudeConstraint, finalCruiseAltitude: Feet): boolean {
return ConstraintUtils.minimumAltitude(constraint) <= finalCruiseAltitude;
}
-
- private *speedConstraintGenerator(
- constraints: MaxSpeedConstraint[],
- sequence: TemporaryCheckpointSequence,
- ): Generator {
- for (let i = constraints.length - 1; i >= 0; ) {
- // Small tolerance here, so we don't get stuck on a constraint
- if (constraints[i].distanceFromStart - sequence.lastCheckpoint.distanceFromStart > -1e-4) {
- i--;
- continue;
- }
-
- yield constraints[i];
- }
- }
-}
-
-export class GeometricPathPlanner {
- static planDescentSegments(
- constraints: DescentAltitudeConstraint[],
- start: GeometricPathPoint,
- end: GeometricPathPoint,
- segments: PlannedGeometricSegment[] = [],
- tolerance: number,
- ): PlannedGeometricSegment[] {
- // A "gradient" is just a quantity of units Feet / NauticalMiles
- const gradient = calculateGradient(start, end);
-
- for (let i = 0; i < constraints.length; i++) {
- const constraintPoint = constraints[i];
-
- if (
- constraintPoint.distanceFromStart >= start.distanceFromStart ||
- constraintPoint.distanceFromStart <= end.distanceFromStart
- ) {
- continue;
- }
-
- const altAtConstraint = start.altitude + gradient * (constraintPoint.distanceFromStart - start.distanceFromStart);
-
- const [isAltitudeConstraintMet, altitudeToContinueFrom] = evaluateAltitudeConstraint(
- constraintPoint.constraint,
- altAtConstraint,
- tolerance,
- );
- if (!isAltitudeConstraintMet) {
- const center = { distanceFromStart: constraintPoint.distanceFromStart, altitude: altitudeToContinueFrom };
-
- this.planDescentSegments(constraints, start, center, segments, tolerance);
- this.planDescentSegments(constraints, center, end, segments, tolerance);
-
- return;
- }
- }
-
- segments.push({ end, gradient });
- }
}
-function evaluateAltitudeConstraint(constraint: AltitudeConstraint, altitude: Feet, tol: number): [boolean, Feet] {
+export function evaluateAltitudeConstraint(
+ constraint: AltitudeConstraint,
+ altitude: Feet,
+ tol: number,
+): [boolean, Feet] {
// Even though in the MCDU constraints count as met if within 250 ft, we use 10 ft here for the initial path construction.
switch (constraint.altitudeDescriptor) {
case AltitudeDescriptor.AtAlt1:
@@ -393,20 +187,3 @@ export const isAltitudeConstraintMet = (constraint: AltitudeConstraint, altitude
return true;
}
};
-
-export type GeometricPathPoint = {
- distanceFromStart: NauticalMiles;
- altitude: Feet;
-};
-
-export type PlannedGeometricSegment = {
- gradient: number;
- end: GeometricPathPoint;
- isTooSteep?: boolean;
-};
-
-export function calculateGradient(start: GeometricPathPoint, end: GeometricPathPoint): number {
- return Math.abs(start.distanceFromStart - end.distanceFromStart) < 1e-9
- ? 0
- : (start.altitude - end.altitude) / (start.distanceFromStart - end.distanceFromStart);
-}
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/DescentStrategy.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/DescentStrategy.ts
index 59108192ff4..5a4182c5883 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/DescentStrategy.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/DescentStrategy.ts
@@ -6,19 +6,16 @@ import { AircraftConfig } from '@fmgc/flightplanning/AircraftConfigTypes';
import { AtmosphericConditions } from '@fmgc/guidance/vnav/AtmosphericConditions';
import { FlightPathAngleStrategy, VerticalSpeedStrategy } from '@fmgc/guidance/vnav/climb/ClimbStrategy';
import { FlapConf } from '@fmgc/guidance/vnav/common';
-import { AircraftConfiguration as AircraftCtlSurfcConfiguration } from '@fmgc/guidance/vnav/descent/ApproachPathBuilder';
+import {
+ AircraftConfiguration as AircraftCtlSurfcConfiguration,
+ DEFAULT_AIRCRAFT_CONTROL_SURFACE_CONFIG,
+} from '@fmgc/guidance/vnav/descent/ApproachPathBuilder';
import { EngineModel } from '@fmgc/guidance/vnav/EngineModel';
import { Predictions, StepResults } from '@fmgc/guidance/vnav/Predictions';
import { VerticalProfileComputationParametersObserver } from '@fmgc/guidance/vnav/VerticalProfileComputationParameters';
import { VnavConfig } from '@fmgc/guidance/vnav/VnavConfig';
import { WindComponent } from '@fmgc/guidance/vnav/wind';
-export const DEFAULT_AIRCRAFT_CONTROL_SURFACE_CONFIG: AircraftCtlSurfcConfiguration = {
- flapConfig: FlapConf.CLEAN,
- speedbrakesExtended: false,
- gearExtended: false,
-};
-
export interface DescentStrategy {
/**
* Computes predictions for a single segment using the atmospheric conditions in the middle.
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/GeometricPathBuilder.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/GeometricPathBuilder.ts
index b5cb4995994..0a9723bf7bc 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/GeometricPathBuilder.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/GeometricPathBuilder.ts
@@ -5,7 +5,6 @@
import { AtmosphericConditions } from '@fmgc/guidance/vnav/AtmosphericConditions';
import { FlightPathAngleStrategy } from '@fmgc/guidance/vnav/climb/ClimbStrategy';
import { FlapConf } from '@fmgc/guidance/vnav/common';
-import { PlannedGeometricSegment } from '@fmgc/guidance/vnav/descent/DescentPathBuilder';
import { StepResults, VnavStepError } from '@fmgc/guidance/vnav/Predictions';
import {
MaxSpeedConstraint,
@@ -14,10 +13,13 @@ import {
} from '@fmgc/guidance/vnav/profile/NavGeometryProfile';
import { TemporaryCheckpointSequence } from '@fmgc/guidance/vnav/profile/TemporaryCheckpointSequence';
import { VerticalProfileComputationParametersObserver } from '@fmgc/guidance/vnav/VerticalProfileComputationParameters';
-import { VnavConfig } from '@fmgc/guidance/vnav/VnavConfig';
import { HeadwindProfile } from '@fmgc/guidance/vnav/wind/HeadwindProfile';
import { MathUtils } from '@flybywiresim/fbw-sdk';
import { AircraftConfig } from '@fmgc/flightplanning/AircraftConfigTypes';
+import { PlannedGeometricSegment } from '@fmgc/guidance/vnav/descent/GeometricPathPlanner';
+import { SpeedProfile } from '@fmgc/guidance/vnav/climb/SpeedProfile';
+import { DescentPathBuilder } from '@fmgc/guidance/vnav/descent/DescentPathBuilder';
+import { BaseGeometryProfile } from '@fmgc/guidance/vnav/profile/BaseGeometryProfile';
export class GeometricPathBuilder {
private flightPathAngleStrategy: FlightPathAngleStrategy;
@@ -25,6 +27,7 @@ export class GeometricPathBuilder {
constructor(
private observer: VerticalProfileComputationParametersObserver,
atmosphericConditions: AtmosphericConditions,
+ private readonly descentPathBuilder: DescentPathBuilder,
private readonly acConfig: AircraftConfig,
) {
this.flightPathAngleStrategy = new FlightPathAngleStrategy(observer, atmosphericConditions, -3.0, this.acConfig);
@@ -33,14 +36,19 @@ export class GeometricPathBuilder {
executeGeometricSegments(
sequence: TemporaryCheckpointSequence,
segments: PlannedGeometricSegment[],
- speedConstraints: MaxSpeedConstraint[],
+ profile: BaseGeometryProfile,
+ speedProfile: SpeedProfile,
windProfile: HeadwindProfile,
) {
+ const speedConstraints = profile.descentSpeedConstraints;
+
const accelerationTargets = this.buildAccelerationTargets(sequence.lastCheckpoint, segments, speedConstraints);
const lastTarget: AccelerationTarget = null;
for (const segment of segments) {
const currentSegmentSequence = new TemporaryCheckpointSequence(sequence.lastCheckpoint);
+ this.flightPathAngleStrategy.flightPathAngle =
+ MathUtils.RADIANS_TO_DEGREES * Math.atan(segment.gradient / 6076.12);
if (
!this.executeGeometricSegment(
@@ -62,24 +70,39 @@ export class GeometricPathBuilder {
lastTarget,
)
) {
- // Marking the segment as too steep, so that we ignore speed constraints on the next try and just fly at the maximum possible gradient
- segment.isTooSteep = true;
-
- this.executeGeometricSegment(
+ // If we cannot do meet the constraint with speedbrakes, build a new descent path to the constraint
+ this.descentPathBuilder.computeManagedDescentPath(
currentSegmentSequence,
- segment,
- accelerationTargets,
+ profile,
+ speedProfile,
windProfile,
- true,
- lastTarget,
+ Infinity,
+ segment.end.distanceFromStart,
);
+ // The path built above is only a "sub-path" of the full descent path, so we want to remove any checkpoints
+ // with significance to the full path
+ currentSegmentSequence.checkpoints.forEach((checkpoint) => {
+ if (
+ checkpoint.reason === VerticalCheckpointReason.GeometricPathStart ||
+ checkpoint.reason === VerticalCheckpointReason.GeometricPathEnd ||
+ checkpoint.reason === VerticalCheckpointReason.IdlePathEnd ||
+ checkpoint.reason === VerticalCheckpointReason.TopOfDescent
+ ) {
+ checkpoint.reason = VerticalCheckpointReason.AtmosphericConditions;
+ }
+ });
+
sequence.push(...currentSegmentSequence.get());
+ sequence.lastCheckpoint.reason = VerticalCheckpointReason.GeometricPathTooSteep;
+
sequence.copyLastCheckpoint({
- reason: VerticalCheckpointReason.GeometricPathTooSteep,
+ reason: VerticalCheckpointReason.AltitudeConstraint,
altitude: segment.end.altitude,
});
+ segment.end.leg.calculated.endsInTooSteepPath = true;
+
continue;
}
}
@@ -97,9 +120,8 @@ export class GeometricPathBuilder {
lastTarget: AccelerationTarget,
) {
const { managedDescentSpeedMach } = this.observer.get();
- this.flightPathAngleStrategy.flightPathAngle = MathUtils.RADIANS_TO_DEGREES * Math.atan(segment.gradient / 6076.12);
- for (let i = 0; i < accelerationTargets.length && !segment.isTooSteep; i++) {
+ for (let i = 0; i < accelerationTargets.length; i++) {
const accelerationTarget = accelerationTargets[i];
const maxDistance = segment.end.distanceFromStart - sequence.lastCheckpoint.distanceFromStart;
@@ -123,22 +145,6 @@ export class GeometricPathBuilder {
);
if (decelerationStep.error === VnavStepError.AVAILABLE_GRADIENT_INSUFFICIENT) {
- if (!useSpeedbrakes) {
- if (VnavConfig.DEBUG_PROFILE) {
- console.log(
- `[FMS/VNAV]: Too steep: ${this.flightPathAngleStrategy.flightPathAngle}°. Trying with speedbrakes.`,
- );
- }
-
- // Break out and try the whole segment with speedbrakes
- return false;
- }
-
- if (VnavConfig.DEBUG_PROFILE) {
- console.log(`[FMS/VNAV]: Too steep: ${this.flightPathAngleStrategy.flightPathAngle}°.`);
- }
-
- segment.gradient = Math.tan(decelerationStep.pathAngle * MathUtils.DEGREES_TO_RADIANS) * 6076.12;
return false;
}
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/GeometricPathPlanner.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/GeometricPathPlanner.ts
new file mode 100644
index 00000000000..513353ecb49
--- /dev/null
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/GeometricPathPlanner.ts
@@ -0,0 +1,67 @@
+import { FlightPlanLeg } from '@fmgc/flightplanning/legs/FlightPlanLeg';
+import { evaluateAltitudeConstraint } from '@fmgc/guidance/vnav/descent/DescentPathBuilder';
+import { DescentAltitudeConstraint } from '@fmgc/guidance/vnav/profile/NavGeometryProfile';
+
+export class GeometricPathPlanner {
+ static planDescentSegments(
+ constraints: DescentAltitudeConstraint[],
+ start: GeometricPathPoint,
+ end: GeometricPathPoint & { leg: FlightPlanLeg },
+ segments: PlannedGeometricSegment[] = [],
+ tolerance: number,
+ ): PlannedGeometricSegment[] {
+ // A "gradient" is just a quantity of units Feet / NauticalMiles
+ const gradient = calculateGradient(start, end);
+
+ for (let i = 0; i < constraints.length; i++) {
+ const constraintPoint = constraints[i];
+
+ if (
+ constraintPoint.distanceFromStart >= start.distanceFromStart ||
+ constraintPoint.distanceFromStart <= end.distanceFromStart
+ ) {
+ continue;
+ }
+
+ const altAtConstraint = start.altitude + gradient * (constraintPoint.distanceFromStart - start.distanceFromStart);
+
+ const [isAltitudeConstraintMet, altitudeToContinueFrom] = evaluateAltitudeConstraint(
+ constraintPoint.constraint,
+ altAtConstraint,
+ tolerance,
+ );
+
+ if (!isAltitudeConstraintMet) {
+ const center = {
+ distanceFromStart: constraintPoint.distanceFromStart,
+ altitude: altitudeToContinueFrom,
+ leg: constraintPoint.leg,
+ };
+
+ this.planDescentSegments(constraints, start, center, segments, tolerance);
+ this.planDescentSegments(constraints, center, end, segments, tolerance);
+
+ return;
+ }
+ }
+
+ segments.push({ end, gradient });
+ }
+}
+
+type GeometricPathPoint = {
+ distanceFromStart: NauticalMiles;
+ altitude: Feet;
+};
+
+export type PlannedGeometricSegment = {
+ gradient: number;
+ end: GeometricPathPoint & { leg: FlightPlanLeg };
+ isTooSteep?: boolean;
+};
+
+function calculateGradient(start: GeometricPathPoint, end: GeometricPathPoint): number {
+ return Math.abs(start.distanceFromStart - end.distanceFromStart) < 1e-9
+ ? 0
+ : (start.altitude - end.altitude) / (start.distanceFromStart - end.distanceFromStart);
+}
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/IdlePathBuilder.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/IdlePathBuilder.ts
new file mode 100644
index 00000000000..eb9b08e803f
--- /dev/null
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/IdlePathBuilder.ts
@@ -0,0 +1,259 @@
+import { MathUtils } from '@flybywiresim/fbw-sdk';
+import { AircraftConfig } from '@fmgc/flightplanning/AircraftConfigTypes';
+import { AtmosphericConditions } from '@fmgc/guidance/vnav/AtmosphericConditions';
+import { SpeedProfile, ManagedSpeedType } from '@fmgc/guidance/vnav/climb/SpeedProfile';
+import { DescentStrategy, IdleDescentStrategy } from '@fmgc/guidance/vnav/descent/DescentStrategy';
+import { StepResults } from '@fmgc/guidance/vnav/Predictions';
+import {
+ VerticalCheckpointReason,
+ MaxSpeedConstraint,
+ VerticalCheckpoint,
+} from '@fmgc/guidance/vnav/profile/NavGeometryProfile';
+import { TemporaryCheckpointSequence } from '@fmgc/guidance/vnav/profile/TemporaryCheckpointSequence';
+import { SpeedLimit } from '@fmgc/guidance/vnav/SpeedLimit';
+import { VerticalProfileComputationParametersObserver } from '@fmgc/guidance/vnav/VerticalProfileComputationParameters';
+import { HeadwindProfile } from '@fmgc/guidance/vnav/wind/HeadwindProfile';
+
+export class IdlePathBuilder {
+ private readonly idleDescentStrategy: DescentStrategy;
+
+ /**
+ * Sometimes we want to check if two distances are close enough to each other to be considered equal, or sometimes we
+ * want to use a distance "just before" or "just after" another distance.
+ */
+ private static readonly DISTANCE_EPSILON = 1e-4;
+
+ constructor(
+ private computationParametersObserver: VerticalProfileComputationParametersObserver,
+ private atmosphericConditions: AtmosphericConditions,
+ private readonly acConfig: AircraftConfig,
+ ) {
+ this.idleDescentStrategy = new IdleDescentStrategy(
+ computationParametersObserver,
+ atmosphericConditions,
+ this.acConfig,
+ );
+ }
+
+ buildIdlePath(
+ sequence: TemporaryCheckpointSequence,
+ descentSpeedConstraints: MaxSpeedConstraint[],
+ speedProfile: SpeedProfile,
+ windProfile: HeadwindProfile,
+ topOfDescentAltitude: Feet,
+ toDistance: NauticalMiles = -Infinity,
+ ): void {
+ // Assume the last checkpoint is the start of the geometric path
+ sequence.copyLastCheckpoint({ reason: VerticalCheckpointReason.IdlePathEnd });
+
+ this.buildIdleSequence(
+ sequence,
+ descentSpeedConstraints,
+ speedProfile,
+ windProfile,
+ topOfDescentAltitude,
+ toDistance,
+ );
+
+ if (sequence.lastCheckpoint.reason === VerticalCheckpointReason.IdlePathAtmosphericConditions) {
+ sequence.lastCheckpoint.reason = VerticalCheckpointReason.TopOfDescent;
+ } else {
+ sequence.copyLastCheckpoint({ reason: VerticalCheckpointReason.TopOfDescent });
+ }
+ }
+
+ private buildIdleSequence(
+ sequence: TemporaryCheckpointSequence,
+ descentSpeedConstraints: MaxSpeedConstraint[],
+ speedProfile: SpeedProfile,
+ windProfile: HeadwindProfile,
+ topOfDescentAltitude: Feet,
+ toDistance: NauticalMiles = -Infinity,
+ ) {
+ const { managedDescentSpeedMach, descentSpeedLimit } = this.computationParametersObserver.get();
+ const speedConstraintsAhead = this.speedConstraintGenerator(descentSpeedConstraints, sequence);
+
+ // We try to figure out what speed we might be decelerating for
+ let previousCasTarget =
+ this.tryGetAnticipatedTarget(
+ sequence,
+ descentSpeedConstraints,
+ speedProfile.shouldTakeDescentSpeedLimitIntoAccount() ? descentSpeedLimit : null,
+ ) ??
+ speedProfile.getTarget(
+ sequence.lastCheckpoint.distanceFromStart,
+ sequence.lastCheckpoint.altitude,
+ ManagedSpeedType.Descent,
+ );
+ let wasPreviouslyUnderSpeedLimitAltitude =
+ speedProfile.shouldTakeDescentSpeedLimitIntoAccount() &&
+ sequence.lastCheckpoint.altitude < descentSpeedLimit.underAltitude;
+
+ for (
+ let i = 0;
+ i < 100 &&
+ !MathUtils.isCloseToLessThan(sequence.lastCheckpoint.distanceFromStart, toDistance) &&
+ !MathUtils.isCloseToLessThan(topOfDescentAltitude, sequence.lastCheckpoint.altitude, 1);
+ i++
+ ) {
+ const { distanceFromStart, altitude, speed, remainingFuelOnBoard } = sequence.lastCheckpoint;
+ const headwind = windProfile.getHeadwindComponent(distanceFromStart, altitude);
+ const isUnderSpeedLimitAltitude =
+ speedProfile.shouldTakeDescentSpeedLimitIntoAccount() && altitude < descentSpeedLimit.underAltitude;
+
+ const casTarget = speedProfile.getTarget(
+ distanceFromStart - IdlePathBuilder.DISTANCE_EPSILON,
+ altitude + IdlePathBuilder.DISTANCE_EPSILON,
+ ManagedSpeedType.Descent,
+ );
+ const currentSpeedTarget = Math.min(
+ casTarget,
+ this.atmosphericConditions.computeCasFromMach(managedDescentSpeedMach, altitude),
+ );
+ const canAccelerate = currentSpeedTarget > speed;
+
+ if (canAccelerate) {
+ // Build acceleration path
+ const speedStep = this.idleDescentStrategy.predictToSpeed(
+ altitude,
+ casTarget,
+ speed,
+ managedDescentSpeedMach,
+ remainingFuelOnBoard,
+ headwind,
+ );
+ const scaling = Math.min(
+ 1,
+ (topOfDescentAltitude - altitude) / (speedStep.finalAltitude - altitude),
+ (toDistance - sequence.lastCheckpoint.distanceFromStart) / speedStep.distanceTraveled,
+ );
+ this.scaleStepBasedOnLastCheckpoint(sequence.lastCheckpoint, speedStep, scaling);
+
+ const didCrossoverSpeedLimitAltitude = wasPreviouslyUnderSpeedLimitAltitude && !isUnderSpeedLimitAltitude;
+ const checkpointReason = didCrossoverSpeedLimitAltitude
+ ? VerticalCheckpointReason.StartDecelerationToLimit
+ : VerticalCheckpointReason.StartDecelerationToConstraint;
+
+ sequence.addDecelerationCheckpointFromStep(speedStep, checkpointReason, previousCasTarget);
+ } else {
+ // Try alt path
+ let finalAltitude = Math.min(altitude + 1500, topOfDescentAltitude);
+
+ if (isUnderSpeedLimitAltitude) {
+ finalAltitude = Math.min(finalAltitude, descentSpeedLimit.underAltitude);
+ }
+
+ const altitudeStep = this.idleDescentStrategy.predictToAltitude(
+ altitude,
+ finalAltitude,
+ speed,
+ managedDescentSpeedMach,
+ remainingFuelOnBoard,
+ headwind,
+ );
+ const scaling = Math.min(
+ 1,
+ (toDistance - sequence.lastCheckpoint.distanceFromStart) / altitudeStep.distanceTraveled,
+ );
+ this.scaleStepBasedOnLastCheckpoint(sequence.lastCheckpoint, altitudeStep, scaling);
+
+ const reason =
+ isUnderSpeedLimitAltitude && altitudeStep.finalAltitude >= descentSpeedLimit.underAltitude
+ ? VerticalCheckpointReason.CrossingDescentSpeedLimit
+ : VerticalCheckpointReason.IdlePathAtmosphericConditions;
+
+ // Check if constraint violated
+ const nextSpeedConstraint = speedConstraintsAhead.next();
+ if (
+ !nextSpeedConstraint.done &&
+ distanceFromStart + altitudeStep.distanceTraveled < nextSpeedConstraint.value.distanceFromStart
+ ) {
+ // Constraint violated
+ const distanceToConstraint = nextSpeedConstraint.value.distanceFromStart - distanceFromStart;
+ const distanceStep = this.idleDescentStrategy.predictToDistance(
+ altitude,
+ distanceToConstraint,
+ speed,
+ managedDescentSpeedMach,
+ remainingFuelOnBoard,
+ headwind,
+ );
+ sequence.addCheckpointFromStep(distanceStep, VerticalCheckpointReason.SpeedConstraint);
+ } else {
+ sequence.addCheckpointFromStep(altitudeStep, reason);
+ }
+ }
+
+ previousCasTarget = casTarget;
+ wasPreviouslyUnderSpeedLimitAltitude = isUnderSpeedLimitAltitude;
+ }
+ }
+
+ // TODO: I really don't know if this function does what it's supposed to, so I hope I don't have to return to it.
+ // The problem it's trying to solve is this: After having built the geometric path to the first violating altitude constraint, we might not be at econ speed yet.
+ // So then we need to accelerate to it on the idle path. However, we want to figure out what the reason for this acceleration is, i.e whether it is due
+ // to the speed limit or a constraint. We need to know this to place the correct checkpoint reason.
+ // What the function does is to try and figure this out based on different criteria.
+ private tryGetAnticipatedTarget(
+ sequence: TemporaryCheckpointSequence,
+ speedConstraints: MaxSpeedConstraint[],
+ speedLimit?: SpeedLimit,
+ ): Knots | null {
+ const {
+ distanceFromStart: pposDistanceFromStart,
+ speed: currentSpeed,
+ altitude: currentAltitude,
+ } = sequence.lastCheckpoint;
+
+ // Find next constraint
+ const nextSpeedConstraint = speedConstraints.find(
+ (c) => c.distanceFromStart >= pposDistanceFromStart && c.maxSpeed <= currentSpeed,
+ );
+
+ const isSpeedLimitValidCandidate =
+ speedLimit && currentAltitude > speedLimit.underAltitude && currentSpeed > speedLimit.speed;
+
+ if (nextSpeedConstraint) {
+ // Try to figure out which speed is more important
+ if (isSpeedLimitValidCandidate) {
+ const altAtConstraint = sequence.interpolateAltitudeBackwards(nextSpeedConstraint.distanceFromStart);
+
+ if (speedLimit.underAltitude > altAtConstraint) {
+ return speedLimit.speed;
+ }
+ }
+
+ return nextSpeedConstraint.maxSpeed;
+ }
+
+ // If we did not find a valid speed constraint candidate, see if the speed limit might be a candidate. If so, return it.
+ if (isSpeedLimitValidCandidate) {
+ return speedLimit.speed;
+ }
+
+ return null;
+ }
+
+ private scaleStepBasedOnLastCheckpoint(lastCheckpoint: VerticalCheckpoint, step: StepResults, scaling: number) {
+ step.distanceTraveled *= scaling;
+ step.fuelBurned *= scaling;
+ step.timeElapsed *= scaling;
+ step.finalAltitude = (1 - scaling) * lastCheckpoint.altitude + scaling * step.finalAltitude;
+ step.speed = (1 - scaling) * lastCheckpoint.speed + scaling * step.speed;
+ }
+
+ private *speedConstraintGenerator(
+ constraints: MaxSpeedConstraint[],
+ sequence: TemporaryCheckpointSequence,
+ ): Generator {
+ for (let i = constraints.length - 1; i >= 0; ) {
+ // Small tolerance here, so we don't get stuck on a constraint
+ if (MathUtils.isCloseToGreaterThan(constraints[i].distanceFromStart, sequence.lastCheckpoint.distanceFromStart)) {
+ i--;
+ continue;
+ }
+
+ yield constraints[i];
+ }
+ }
+}
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/TacticalDescentPathBuilder.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/TacticalDescentPathBuilder.ts
index 1fa12ec9e44..5aeb6daa143 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/TacticalDescentPathBuilder.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/descent/TacticalDescentPathBuilder.ts
@@ -7,7 +7,7 @@ import { AircraftConfig } from '@fmgc/flightplanning/AircraftConfigTypes';
import { AtmosphericConditions } from '@fmgc/guidance/vnav/AtmosphericConditions';
import { VerticalSpeedStrategy } from '@fmgc/guidance/vnav/climb/ClimbStrategy';
import { SpeedProfile } from '@fmgc/guidance/vnav/climb/SpeedProfile';
-import { AircraftConfiguration, AircraftConfigurationProfile } from '@fmgc/guidance/vnav/descent/ApproachPathBuilder';
+import { AircraftConfiguration, AircraftConfigurationRegister } from '@fmgc/guidance/vnav/descent/ApproachPathBuilder';
import { DescentStrategy } from '@fmgc/guidance/vnav/descent/DescentStrategy';
import { StepResults } from '@fmgc/guidance/vnav/Predictions';
import { BaseGeometryProfile } from '@fmgc/guidance/vnav/profile/BaseGeometryProfile';
@@ -605,6 +605,8 @@ class PhaseTable {
phases: SubPhase[] = [];
+ private readonly configuration: AircraftConfigurationRegister = new AircraftConfigurationRegister();
+
constructor(
private readonly parameters: VerticalProfileComputationParameters,
private readonly winds: HeadwindProfile,
@@ -627,12 +629,10 @@ class PhaseTable {
sequence.lastCheckpoint.distanceFromStart,
sequence.lastCheckpoint.altitude,
);
- const configuration = AircraftConfigurationProfile.getBySpeed(sequence.lastCheckpoint.speed, this.parameters);
-
const phaseResult = phase.execute(phase.shouldFlyAsLevelSegment ? levelFlightStrategy : descentStrategy)(
sequence.lastCheckpoint,
headwind,
- configuration,
+ this.configuration.setFromSpeed(sequence.lastCheckpoint.speed, this.parameters),
);
if (phase instanceof DescendingDeceleration) {
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/profile/NavGeometryProfile.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/profile/NavGeometryProfile.ts
index 5343b97adb3..51cdc4a8782 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/profile/NavGeometryProfile.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/profile/NavGeometryProfile.ts
@@ -9,6 +9,7 @@ import { isAltitudeConstraintMet } from '@fmgc/guidance/vnav/descent/DescentPath
import { FlightPlanService } from '@fmgc/flightplanning/FlightPlanService';
import { AltitudeConstraint, SpeedConstraint } from '@fmgc/flightplanning/data/constraint';
import { AltitudeDescriptor } from '@flybywiresim/fbw-sdk';
+import { FlightPlanLeg } from '@fmgc/flightplanning/legs/FlightPlanLeg';
// TODO: Merge this with VerticalCheckpoint
export interface VerticalWaypointPrediction {
@@ -128,6 +129,7 @@ export interface MaxSpeedConstraint {
export interface DescentAltitudeConstraint {
distanceFromStart: NauticalMiles;
constraint: AltitudeConstraint;
+ leg: FlightPlanLeg;
}
export interface GeographicCruiseStep {
diff --git a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/profile/TemporaryCheckpointSequence.ts b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/profile/TemporaryCheckpointSequence.ts
index d605b8cd845..57961f0bf08 100644
--- a/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/profile/TemporaryCheckpointSequence.ts
+++ b/fbw-a32nx/src/systems/fmgc/src/guidance/vnav/profile/TemporaryCheckpointSequence.ts
@@ -82,7 +82,7 @@ export class TemporaryCheckpointSequence {
return this.checkpoints[0].altitude;
}
- for (let i = 1; i < this.checkpoints.length - 1; i++) {
+ for (let i = 1; i < this.checkpoints.length; i++) {
if (distanceFromStart >= this.checkpoints[i].distanceFromStart) {
return Common.interpolate(
distanceFromStart,
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/FMC/FlightManagementComputer.ts b/fbw-a380x/src/systems/instruments/src/MFD/FMC/FlightManagementComputer.ts
index c2e05a204e2..886a63da3a5 100644
--- a/fbw-a380x/src/systems/instruments/src/MFD/FMC/FlightManagementComputer.ts
+++ b/fbw-a380x/src/systems/instruments/src/MFD/FMC/FlightManagementComputer.ts
@@ -890,6 +890,7 @@ export class FlightManagementComputer implements FmcInterface {
this.acInterface.updatePerfSpeeds();
this.acInterface.updateWeights();
this.acInterface.toSpeedsChecks();
+ this.acInterface.checkTooSteepPath();
const toFlaps = this.fmgc.getTakeoffFlapsSetting();
if (toFlaps) {
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/FMC/FmcAircraftInterface.ts b/fbw-a380x/src/systems/instruments/src/MFD/FMC/FmcAircraftInterface.ts
index e2d1092e4d3..94436dadc13 100644
--- a/fbw-a380x/src/systems/instruments/src/MFD/FMC/FmcAircraftInterface.ts
+++ b/fbw-a380x/src/systems/instruments/src/MFD/FMC/FmcAircraftInterface.ts
@@ -1780,6 +1780,24 @@ export class FmcAircraftInterface {
return SimVar.SetSimVarValue('L:A32NX_FM_LS_COURSE', 'number', course);
}
+
+ private hasTooSteepPathAhead = false;
+
+ checkTooSteepPath() {
+ const hasTooSteepPathAhead = this.fmc.guidanceController?.vnavDriver?.shouldShowTooSteepPathAhead();
+
+ if (hasTooSteepPathAhead !== this.hasTooSteepPathAhead) {
+ this.hasTooSteepPathAhead = hasTooSteepPathAhead;
+
+ if (hasTooSteepPathAhead) {
+ this.fmc.addMessageToQueue(
+ NXSystemMessages.tooSteepPathAhead,
+ () => !this.fmc.guidanceController?.vnavDriver?.shouldShowTooSteepPathAhead(),
+ undefined,
+ );
+ }
+ }
+ }
}
class FmArinc429OutputWord extends Arinc429Word {
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFpln.tsx b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFpln.tsx
index 165dcc272c6..b3f07bfacc4 100644
--- a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFpln.tsx
+++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFpln.tsx
@@ -362,6 +362,14 @@ export class MfdFmsFpln extends FmsPage {
leg?.calculated?.cumulativeDistanceWithTransitions ??
lastDistanceFromStart + (this.derivedFplnLegData[i].distanceFromLastWpt ?? 0) - reduceDistanceBy;
this.lineData.push(data);
+
+ if (leg.calculated?.endsInTooSteepPath) {
+ this.lineData.push({
+ type: FplnLineType.Special,
+ originalLegIndex: null,
+ label: 'TOO STEEP PATH',
+ });
+ }
} else {
const data: FplnLineSpecialDisplayData = {
type: FplnLineType.Special,
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnVertRev.tsx b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnVertRev.tsx
index 2f02e40a73d..45dd0f993bd 100644
--- a/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnVertRev.tsx
+++ b/fbw-a380x/src/systems/instruments/src/MFD/pages/FMS/F-PLN/MfdFmsFplnVertRev.tsx
@@ -165,6 +165,10 @@ export class MfdFmsFplnVertRev extends FmsPage {
1;
if (wptIdx !== undefined) {
const leg = this.loadedFlightPlan.legElementAt(wptIdx);
+ const previousElement = this.loadedFlightPlan.maybeElementAt(wptIdx - 1);
+ const isPartOfTooSteepPathSegment =
+ leg.calculated?.endsInTooSteepPath ||
+ (previousElement?.isDiscontinuity === false && previousElement.calculated?.endsInTooSteepPath);
if (!MfdFmsFplnVertRev.isEligibleForVerticalRevision(wptIdx, leg, this.loadedFlightPlan)) {
this.speedMessageArea.set(`SPD CSTR NOT ALLOWED AT ${leg.ident}`);
@@ -175,7 +179,7 @@ export class MfdFmsFplnVertRev extends FmsPage {
}
this.speedMessageArea.set('');
this.spdConstraintDisabled.set(false);
- this.altitudeMessageArea.set('');
+ this.altitudeMessageArea.set(isPartOfTooSteepPathSegment ? 'TOO STEEP PATH AHEAD' : '');
this.altConstraintDisabled.set(false);
// Load speed constraints
diff --git a/fbw-a380x/src/systems/instruments/src/MFD/shared/NXSystemMessages.ts b/fbw-a380x/src/systems/instruments/src/MFD/shared/NXSystemMessages.ts
index f99c82da4e2..0360ddae63d 100644
--- a/fbw-a380x/src/systems/instruments/src/MFD/shared/NXSystemMessages.ts
+++ b/fbw-a380x/src/systems/instruments/src/MFD/shared/NXSystemMessages.ts
@@ -110,6 +110,7 @@ export const NXSystemMessages = {
stepAboveMaxFl: new TypeIIMessage('STEP ABOVE MAX FL'),
stepAhead: new TypeIIMessage('STEP AHEAD'),
stepDeleted: new TypeIIMessage('STEP DELETED'),
+ tooSteepPathAhead: new TypeIIMessage('TOO STEEP PATH AHEAD'),
};
export const NXFictionalMessages = {
diff --git a/fbw-common/src/systems/shared/src/FmMessages.ts b/fbw-common/src/systems/shared/src/FmMessages.ts
index d99acfb683f..379ef464dc2 100644
--- a/fbw-common/src/systems/shared/src/FmMessages.ts
+++ b/fbw-common/src/systems/shared/src/FmMessages.ts
@@ -218,4 +218,10 @@ export const FMMessageTypes: Readonly> = {
color: 'White',
clearable: true,
},
+ TooSteepPathAhead: {
+ id: 20,
+ text: 'TOO STEEP PATH AHEAD',
+ color: 'Amber',
+ clearable: true,
+ },
};
diff --git a/fbw-common/src/systems/shared/src/MathUtils.ts b/fbw-common/src/systems/shared/src/MathUtils.ts
index a835dee0608..30b79c90bfb 100644
--- a/fbw-common/src/systems/shared/src/MathUtils.ts
+++ b/fbw-common/src/systems/shared/src/MathUtils.ts
@@ -595,4 +595,57 @@ export class MathUtils {
return MathUtils.interpolate(j, table[0][c1], table[0][c2], interpolatedRowAtC1, interpolatedRowAtC2);
}
+
+ /**
+ * Checks whether two numbers are within a certain epsilon of each other.
+ * @param a
+ * @param b
+ * @param epsilon the absolute tolerance
+ * @returns true if the numbers are within epsilon of each other
+ */
+ public static isAboutEqual(a: number, b: number, epsilon = 1e-4): boolean {
+ return Math.abs(a - b) < epsilon;
+ }
+
+ /**
+ * Checks whether a number is positive and within a certain epsilon of zero.
+ * @param num
+ * @param epsilon the absolute tolerance
+ * @returns true if the number is positive and within epsilon of zero
+ */
+ public static isCloseToPositive(num: number, epsilon = 1e-4): boolean {
+ return num > -Math.abs(epsilon);
+ }
+
+ /**
+ * Checks whether a number is negative and within a certain epsilon of zero.
+ * @param num
+ * @param epsilon the absolute tolerance
+ * @returns true if the number is negative and within epsilon of zero
+ */
+ public static isCloseToNegative(num: number, epsilon = 1e-4): boolean {
+ return this.isCloseToPositive(-num, epsilon);
+ }
+
+ /**
+ * Checks whether a > b or a is within a certain epsilon of b.
+ * @param a
+ * @param b
+ * @param epsilon the absolute tolerance
+ * @returns true if the number is greater than or within epsilon of the other
+ */
+ public static isCloseToGreaterThan(a: number, b: number, epsilon = 1e-4): boolean {
+ return this.isCloseToPositive(a - b, epsilon);
+ }
+
+ /**
+ * Checks whether a < b or a is within a certain epsilon of b.
+ * @param a
+ * @param b
+ * @param epsilon the absolute tolerance
+ * @returns true if the number is less than or within epsilon of the other
+ */
+ public static isCloseToLessThan(a: number, b: number, epsilon = 1e-4): boolean {
+ return this.isCloseToNegative(a - b, epsilon);
+ }
}