From 8d1d9eec9b2f2e0ae15dc20f21c943e7f9693c89 Mon Sep 17 00:00:00 2001
From: pwarszawski
Date: Sat, 7 Nov 2026 18:10:47 +0100
Subject: [PATCH] AI braking: smaller steps, earlier, while approaching a stop
or a lower speed
Changes to how the AI works the train brake on the way to a lower speed
or a stop, so that it uses few, early applications instead of a late
deep one:
- Approaching a lower speed, the deceleration may fall short of the
demand by only half of a step (fBrake_a1) before the brake is applied
harder. With a full step the demand crept up meanwhile and a long G
train got its next step many seconds too late.
- The shortfall is checked every 3 s. If it keeps growing at a handle
left alone for longer than the brake takes to respond, the AI adds a
small step at once instead of waiting until the margin calls for a
deep one. A shortfall that shrinks on its own (brake still filling,
blocks gripping harder at lower speed) does not trigger it.
- Releasing, a position short of the first one counts as giving the
minimum stage of the distributor, not less.
- Holding the speed on a descent, the predicted excess speed is taken
off over the time the brake takes to respond, not at once, so a slow
brake does not get a deep application that then takes as long to
release.
---
vehicle/Driver.cpp | 69 ++++++++++++++++++++++++++++++++++++++++------
vehicle/Driver.h | 5 ++++
2 files changed, 65 insertions(+), 9 deletions(-)
diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp
index 8d2a3fac6..ea78e655c 100644
--- a/vehicle/Driver.cpp
+++ b/vehicle/Driver.cpp
@@ -2928,6 +2928,9 @@ bool TController::ReleaseEngine() {
return OK;
}
+// defined further down, with the braking demand it is used for
+static double brake_response_horizon( TMoverParameters const *Vehicle, TBrakeSystem const Brakesystem, double const Acceleration, double const Brakea0 );
+
bool TController::IncBrake()
{ // zwiększenie hamowania
auto OK { false };
@@ -3040,7 +3043,7 @@ bool TController::IncBrake()
}
double deltaAcc = -AccDesired*BrakeAccFactor() - (fBrake_a0[0] + 4.0 * (/*GBH mvOccupied->BrakeCtrlPosR*/BrakeCtrlPosition - 1 - pos_corr)*fBrake_a1[0]);
- if( deltaAcc > fBrake_a1[0])
+ if( deltaAcc > brake_increase_margin() )
{
if( /*GBH mvOccupied->BrakeCtrlPosR*/BrakeCtrlPosition < 0.1 ) {
OK = /*mvOccupied->*/BrakeLevelAdd( BrakingInitialLevel ); //GBH
@@ -3069,10 +3072,13 @@ bool TController::IncBrake()
}
else if( BrakeCtrlPosition > 0.0
&& AccDesired < 0.0 // only while braking is asked for, not when called for a train slipping back while starting
- && fBrakeHeldTime > (
- mvOccupied->BrakeDelayFlag > bdelay_G ?
- mvOccupied->BrakeDelay[ 1 ] :
- mvOccupied->BrakeDelay[ 3 ] ) ) {
+ && ( fBrakeHeldTime > (
+ mvOccupied->BrakeDelayFlag > bdelay_G ?
+ mvOccupied->BrakeDelay[ 1 ] :
+ mvOccupied->BrakeDelay[ 3 ] )
+ // a train falling further behind needs no full fill time to be seen not to get enough
+ || ( ( fShortfall > 0.02 )
+ && ( fBrakeHeldTime > brake_response_horizon( mvOccupied, BrakeSystem, 1.0, fBrake_a0[ 0 ] ) ) ) ) ) {
// the table says the present position is enough, but the brake has had its full fill
// time since the handle last moved and the train is still not slowing down as asked:
// believe the train rather than the table
@@ -3194,7 +3200,9 @@ bool TController::DecBrake() {
if( fBrake_a0[0] != 0.0
|| fBrake_a1[0] != 0.0 ) {
auto const pos_diff { ( is_dmu() ? 0.25 : 1.0 ) };
- deltaAcc = -AccDesired * BrakeAccFactor() - ( fBrake_a0[ 0 ] + 4 * (/*GBH mvOccupied->BrakeCtrlPosR*/BrakeCtrlPosition - pos_diff )*fBrake_a1[ 0 ] );
+ // a distributor applied at all gives at least its minimum stage, about what the first position does, so
+ // a position short of the first one brakes no weaker than that
+ deltaAcc = -AccDesired * BrakeAccFactor() - ( fBrake_a0[ 0 ] + 4 * std::max( 0.0, BrakeCtrlPosition - pos_diff ) * fBrake_a1[ 0 ] );
}
if (deltaAcc < 0 && /*GBH mvOccupied->BrakeCtrlPosR*/ BrakeCtrlPosition > 0) {
OK = /*mvOccupied->*/ BrakeLevelAdd(-0.25);
@@ -7701,7 +7709,7 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) {
// train is still speeding up, the prediction above has taken that off already)
auto room { brakingroom };
if( ( AbsAccS <= 0.0 )
- && ( ( VelNext > 0.0 ) || ( BrakeCtrlPosition <= 0.0 ) ) ) { // for a stop, only while the brake is still to be applied
+ && ( BrakeCtrlPosition <= 0.0 ) ) { // only while the brake is still to be applied: once it acts, the room it needs to respond is behind
room = std::max(
0.5 * brakingroom,
brakingroom - vel / 3.6 * brake_response_horizon( mvOccupied, BrakeSystem, 1.0, fBrake_a0[ 0 ] ) );
@@ -7830,6 +7838,10 @@ TController::adjust_desired_speed_for_current_speed() {
BrakeSystem == TBrakeSystem::Pneumatic ?
std::clamp( fAccGravity * 15.0, 1.0, 3.0 ) :
std::clamp( fAccGravity * 7.5, 1.0, 1.5 ) ) };
+ // the predicted difference is to be made up over the time the brake takes to respond, not at once: with a
+ // slow brake, a demand sized to remove it at once comes on as a deep application, which then takes as
+ // long to let go, and the train ends up well below the speed it was to hold
+ auto const holdgain { std::min( 0.04, 1.0 / ( 3.6 * std::max( 1.0, horizon ) ) ) };
if( ( speedestimate > VelDesired )
&& ( VelDesired == 0.0 ) ) {
// jesli stoj, to hamuj, ale i tak juz za pozno :)
@@ -7843,7 +7855,7 @@ TController::adjust_desired_speed_for_current_speed() {
AccDesired = std::min(
AccDesired, // but don't override decceleration for VelNext
std::clamp(
- EU07_AI_NOACCELERATION - 0.04 * ( speedestimate - holdspeed ),
+ EU07_AI_NOACCELERATION - holdgain * ( speedestimate - holdspeed ),
-0.05 + fAccThreshold,
std::max( EU07_AI_NOACCELERATION, AccPreferred ) ) );
// HACK: for cargo trains with high braking threshold ensure we cross that threshold
@@ -8151,6 +8163,20 @@ void TController::increase_tractive_force() {
cue_action( driver_hint::tractiveforceincrease );
}
+// how far the deceleration may fall short of the demand before the brake is applied harder. approaching a lower
+// speed, a pneumatic brake given a full step only once that much is missing acts too late: the demand
+// creeps up meanwhile, and in a long G train the next step takes many seconds to act, so half a step is enough
+double
+TController::brake_increase_margin() const {
+
+ if( ( BrakeSystem == TBrakeSystem::Pneumatic )
+ && ( VelNext > 0.0 )
+ && ( VelNext < DirectionalVel() ) ) {
+ return 0.5 * fBrake_a1[ 0 ];
+ }
+ return fBrake_a1[ 0 ];
+}
+
void TController::control_braking_force() {
// don't touch the brakes when compressing buffers during coupling/uncoupling
@@ -8168,6 +8194,19 @@ void TController::control_braking_force() {
// resultant the AI asks for, not with the brake-only demand AccDesired holds on a descent
auto const accdesiredresultant { AccDesired + fAccDesiredGravity };
+ // every few seconds, note how far the train falls short of the demand and whether that is getting worse: a brake
+ // still filling, or blocks gripping harder as the train slows down, make up a shortfall on their own, but a
+ // shortfall that keeps growing at a constant handle means the train is falling further behind its braking curve
+ if( ElapsedTime - fShortfallTime >= 3.0 ) {
+ auto const shortfall { AbsAccS - accdesiredresultant };
+ iShortfallGrowth = (
+ ( ( AccDesired < 0.0 ) && ( shortfall > fShortfall + 0.005 ) ) ?
+ iShortfallGrowth + 1 :
+ 0 );
+ fShortfall = shortfall;
+ fShortfallTime = ElapsedTime;
+ }
+
if( is_emu() && !ForcePNBrake ) {
// właściwie, to warunek powinien być na działający EP
// Ra: to dobrze hamuje EP w EZT
@@ -8201,7 +8240,7 @@ void TController::control_braking_force() {
} // type & dt_ezt
else {
// a stara wersja w miarę dobrze działa na składy wagonowe
- if( (accdesiredresultant < fAccGravity - 0.1 && AbsAccS > accdesiredresultant + fBrake_a1[0] // regular braking
+ if( (accdesiredresultant < fAccGravity - 0.1 && AbsAccS > accdesiredresultant + brake_increase_margin() // regular braking
|| fAccGravity < -0.05 && velocity < -0.1) && // also brake if uphill and slipping back
(fBrakeTime < 0.0
// the size of the brake demand decides whether to skip the reaction time, so the brake-only part
@@ -8214,6 +8253,18 @@ void TController::control_braking_force() {
// Ra: ten czas należy zmniejszyć, jeśli czas dojazdu do zatrzymania jest mniejszy
(3.0 + 0.5 * ((mvOccupied->BrakeDelayFlag > bdelay_G ? mvOccupied->BrakeDelay[1] : mvOccupied->BrakeDelay[3]) - 3.0)) * 0.5); // Ra: tymczasowo, bo przeżyna S1
}
+ else if( ( BrakeSystem == TBrakeSystem::Pneumatic )
+ && ( AccDesired < 0.0 )
+ && ( BrakeCtrlPosition > 0.0 )
+ && ( fShortfall > 0.02 )
+ && ( iShortfallGrowth >= 2 )
+ && ( fBrakeHeldTime > brake_response_horizon( mvOccupied, BrakeSystem, 1.0, fBrake_a0[ 0 ] ) ) ) {
+ // the train has been falling further behind the demand for a while at a handle left alone for longer than
+ // the brake takes to respond: a small step now, while it still is small, rather than the deep one the
+ // margin above would allow only once the train is far behind
+ cue_action( driver_hint::brakingforceincrease );
+ iShortfallGrowth = 0;
+ }
if ( (accdesiredresultant < fAccGravity - 0.05
|| AccDesired >= EU07_AI_NOACCELERATION) // no braking asked for, yet the train is slowing down more than that
&& accdesiredresultant - fBrake_a1[0] * 0.51 - AbsAccS > 0.05 && OrderCurrentGet() != Disconnect && VelDesired > 0.0 ) {
diff --git a/vehicle/Driver.h b/vehicle/Driver.h
index 11a01517f..f1975fca6 100644
--- a/vehicle/Driver.h
+++ b/vehicle/Driver.h
@@ -311,6 +311,8 @@ private:
double DirectionalVel() const {
return mvOccupied->Vel * sign( iDirection * mvOccupied->V ); }
+ double brake_increase_margin() const;
+
void update_timers( double const dt );
void update_logs( double const dt );
void determine_consist_state();
@@ -401,6 +403,9 @@ private:
double VelDesired = 0.0; // predkość, z jaką ma jechać, wynikająca z analizy tableki; <=VelSignal
double fAccDesiredAv = 0.0; // uśrednione przyspieszenie z kolejnych przebłysków świadomości, żeby ograniczyć migotanie
double fBrakeHeldTime = 0.0; // czas od ostatniego przestawienia kranu hamulca zasadniczego
+ double fShortfall = 0.0; // how far the train fell short of the braking demand at the last check [m/s2]
+ double fShortfallTime = 0.0; // when that was checked
+ int iShortfallGrowth = 0; // how many checks in a row the shortfall has grown
double fAccDesiredGravity = 0.0; // część AccDesired odjęta na spadku jako kompensacja pochylenia; AbsAccS zawiera ją już sam
double VelforDriver = -1.0; // prędkość, używana przy zmianie kierunku (ograniczenie przy nieznajmości szlaku?)
double VelSignal = 0.0; // ograniczenie prędkości z kompilacji znaków i sygnałów // normalnie na początku ma stać, no chyba że jedzie