From 36d4f16c01abd9404c7e3b59547260f33191b0ba Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Sun, 27 Sep 2026 17:50:07 +0200 Subject: [PATCH 01/13] Brake force table: take the rapid step threshold from the distributor TMoverParameters::BrakeForceR() halves the force of a vehicle set to R below 40 km/h. That is one approximation for every distributor, but a NESt3/ESt4 distributor with a rapid relay engages the step above 70 km/h and keeps it down to 50 km/h (TNESt3::GetPF, RapidStatus), and below that its cylinder pressure is divided by the relay's own multiplier (TRapid, RapidMult). For a vehicle braking from speed the full force therefore lasts down to 50 km/h, not 40. The AI builds its consist brake table (fBrake_a0/a1) from BrakeForceR(), so between 40 and 50 km/h it expected twice the force the wagons actually deliver. Add TBrake::GetRapidRatio(), which keeps the old approximation as the default, and override it in TNESt3 with the relay's threshold and multiplier. The brake itself is unchanged; only the prediction is. --- McZapkie/Mover.cpp | 4 ++-- McZapkie/Oerlikon_ESt.cpp | 11 +++++++++++ McZapkie/Oerlikon_ESt.h | 2 ++ McZapkie/hamulce.h | 5 +++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 83c918e24..a58878ab4 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -5166,8 +5166,8 @@ double TMoverParameters::BrakeForceR(double ratio, double velocity) press = MaxBrakePress[3]; if (DynamicBrakeType == dbrake_automatic) ratio = ratio + (1.5 - ratio) * std::min(1.0, Vel * 0.02); - if (BrakeDelayFlag & bdelay_R && BrakeMethod % 128 != bp_Cosid && BrakeMethod % 128 != bp_D1 && BrakeMethod % 128 != bp_D2 && Power < 1 && velocity < 40) - ratio = ratio / 2; + if (BrakeDelayFlag & bdelay_R && BrakeMethod % 128 != bp_Cosid && BrakeMethod % 128 != bp_D1 && BrakeMethod % 128 != bp_D2 && Power < 1) + ratio *= Hamulec->GetRapidRatio(velocity); if (TrainType == dt_DMU && velocity < 30.0) { ratio -= 0.3; diff --git a/McZapkie/Oerlikon_ESt.cpp b/McZapkie/Oerlikon_ESt.cpp index 9559d3052..d8d5b3da8 100644 --- a/McZapkie/Oerlikon_ESt.cpp +++ b/McZapkie/Oerlikon_ESt.cpp @@ -324,6 +324,17 @@ void TNESt3::Init( double const PP, double const HPP, double const LPP, double c RapidStaly = false; } +double TNESt3::GetRapidRatio(double const Vel) const +{ + // ESt3 has no rapid relay, and with disc brakes the rapid step stays engaged at all speeds + if( ( PrzekRapid == nullptr ) || ( true == RapidStaly ) ) { + return 1.0; + } + // a braking vehicle comes from above, so the step is still engaged down to the speed + // at which GetPF() drops it, not the one at which it engages + return ( Vel < 50.0 ? 1.0 / PrzekRapid->GetRapidMult() : 1.0 ); +} + double TNESt3::GetCRP() { return CntrlRes->P(); diff --git a/McZapkie/Oerlikon_ESt.h b/McZapkie/Oerlikon_ESt.h index c12691851..180c47206 100644 --- a/McZapkie/Oerlikon_ESt.h +++ b/McZapkie/Oerlikon_ESt.h @@ -117,6 +117,7 @@ class TRapid : public TPrzekladnik { public: void SetRapidParams(double mult, double size); void SetRapidStatus(bool rs); + double GetRapidMult() const { return RapidMult; } void Update(double dt) /*override*/; inline TRapid() : TPrzekladnik() @@ -213,6 +214,7 @@ class TNESt3 : public TBrake { virtual double GetPF( double const PP, double const dt, double const Vel ) /*override*/; // przeplyw miedzy komora wstepna i PG void EStParams(double i_crc); // parametry charakterystyczne dla ESt virtual double GetCRP() /*override*/; + double GetRapidRatio(double const Vel) const override; void CheckState(double const BCP, double &dV1); // glowny przyrzad rozrzadczy void CheckReleaser(double const dt); // odluzniacz double CVs(double const BP); // napelniacz sterujacego diff --git a/McZapkie/hamulce.h b/McZapkie/hamulce.h index a05a36359..11b8bdfb3 100644 --- a/McZapkie/hamulce.h +++ b/McZapkie/hamulce.h @@ -442,6 +442,11 @@ class TBrake /// Sets the rapid step ratio. Default no-op; overridden where supported. /// Rapid ratio. virtual void SetRM(double const RMR) {}; // ustalenie przelozenia rapida + /// Share of the full cylinder pressure the distributor delivers at the given speed + /// while the vehicle is slowing down through it, for a vehicle set to R. Used only to predict + /// the brake force; the default is the approximation BrakeForceR() has always used. + /// Vehicle velocity [km/h]. + virtual double GetRapidRatio(double const Vel) const { return Vel < 40.0 ? 0.5 : 1.0; } /// Sets the velocity threshold for the rapid step. /// Velocity threshold (same unit as Vel passed to GetPF). virtual void SetRV(double const RVR) From 92bd7ef827196e0ae7f3dfa6e5cbabf93eb87390 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 28 Sep 2026 15:45:10 +0200 Subject: [PATCH 02/13] AI braking: hold the speed on a descent Two changes that only work together: the first on its own lets a heavy freight train brake less on a descent than it needs (in testing an ET42 with 30 loaded wagons overran the stop signal by 16-48 m at 20 per mille), the second makes the hold on a descent what it should be. The comparison that decides whether the AI may skip its reaction time between two brake steps stays on the brake-only demand, as before: it is about how much brake is asked for, and a heavy train in G approaching a stop on a descent needs its brake built up at once. Compared in the resultant frame, that train built up its brake one step per reaction time and overran the signal (16 copies of the ET42 train, x20: 22 of 32 overruns with the comparison moved, 0 of 48 with it kept). 1. Compare the measured deceleration in the same frame. Going downhill, adjust_desired_speed_for_current_speed() subtracts the gravity component from AccDesired, so that IncBrake() sizes the handle position for what the brake alone has to deliver. control_braking_force() then compares that brake-only value with AbsAccS, which is the resultant acceleration and already contains the gravity. On a 20 per mille descent the two differ by about 0.2 m/s2, so the AI asked for more braking than the target needed and then released, and repeated that. Keep the subtracted part in fAccDesiredGravity and add it back wherever AccDesired is compared with AbsAccS. It is zero everywhere else (flat, uphill, braking test), so the behaviour there does not change. 2. Hold the speed with one continuous law. On a descent the AI looks ahead to decide whether it will exceed the target speed. The look-ahead was (1 - a0) * 30 / 3.6 s, a few seconds, while a pneumatic brake in G takes tens of seconds to apply or to release; the AI therefore acted on an estimate the train had already left behind. The demand then jumped between the braking threshold above the target and a lerp towards full acceleration below it, and with the brake that slow this turned into long alternating applications and releases. - look ahead for half the brake application time while the train is speeding up and half the release time while it is slowing down (brake_response_horizon), never less than before; - use one law on both sides of the target, proportional to the predicted difference, and hold a band just below the limit that widens with the gradient (pneumatic up to 3 km/h, EP/electric up to 1.5 km/h); - once below a lower limit ahead, hold that limit instead of speeding up towards the line speed and braking again. --- vehicle/Driver.cpp | 101 ++++++++++++++++++++++++++++++++++----------- vehicle/Driver.h | 1 + 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 9a6de8596..5b61b6dd1 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -7483,6 +7483,25 @@ TController::adjust_desired_speed_for_limits() { } } +// how long the present acceleration of a consist lasts whatever is done with the handle now: half the time +// its pneumatic brake takes to apply (while speeding up) or to release (while slowing down), which in G +// is tens of seconds; never shorter than the look-ahead the AI has always used +static double +brake_response_horizon( TMoverParameters const *Vehicle, TBrakeSystem const Brakesystem, double const Acceleration, double const Brakea0 ) { + + auto horizon { ( 1.0 - Brakea0 ) * 30.0 / 3.6 }; + if( Brakesystem == TBrakeSystem::Pneumatic ) { + auto const isrelease { Acceleration < 0.0 }; + horizon = std::max( + horizon, + 0.5 * ( + Vehicle->BrakeDelayFlag > bdelay_G ? + Vehicle->BrakeDelay[ isrelease ? 0 : 1 ] : + Vehicle->BrakeDelay[ isrelease ? 2 : 3 ] ) ); + } + return horizon; +} + void TController::adjust_desired_speed_for_target_speed( double const Range ) { // ustalanie zadanego przyspieszenia @@ -7496,6 +7515,7 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) { // gdy jedzie wolniej niż potrzeba, albo nie ma przeszkód na drodze // normalna jazda AccDesired = VelDesired != 0.0 ? AccPreferred : -0.01; + fAccDesiredGravity = 0.0; auto const vel { DirectionalVel() }; @@ -7654,35 +7674,57 @@ TController::adjust_desired_speed_for_current_speed() { if( fAccGravity > 0.025 ) { // going sharply downhill we may need to start braking sooner than usual // try to estimate increase of current velocity before engaged brakes start working - auto const speedestimate = vel + ( 1.0 - fBrake_a0[ 0 ] ) * 30.0 * AbsAccS; - if( speedestimate > VelDesired ) { - // jesli jedzie za szybko do AKTUALNEGO - if( VelDesired == 0.0 ) { - // jesli stoj, to hamuj, ale i tak juz za pozno :) - AccDesired = std::min( AccDesired, -0.85 ); // hamuj solidnie - } - else { - // if it looks like we'll exceed maximum speed start thinking about slight slowing down - AccDesired = std::min( AccDesired, -0.05 + fAccThreshold ); - // HACK: for cargo trains with high braking threshold ensure we cross that threshold - if( true == IsCargoTrain - && fBrake_a0[0] > 0.2 ) { - AccDesired -= std::clamp( fBrake_a0[ 0 ] - 0.2, 0.0, 0.15 ); - } - } + // look ahead for as long as the brake takes to respond, since that is how long the + // present acceleration will last whatever is done with the handle now; a pneumatic + // brake applies and releases on its own time scale, which in G is tens of seconds + auto const horizon { brake_response_horizon( mvOccupied, BrakeSystem, AbsAccS, fBrake_a0[ 0 ] ) }; + auto const speedestimate = vel + 3.6 * horizon * AbsAccS; + // already below a lower limit ahead, the train has no business speeding up towards it + auto holdlimit { VelDesired }; + if( ( VelNext > 0.0 ) + && ( VelNext < holdlimit ) + && ( vel <= VelNext + fVelPlus ) + && ( ActualProximityDist < std::max( 2.0 * fBrakeDist, 500.0 ) ) ) { + holdlimit = VelNext; + } + // hold a band just below the limit; the steeper the descent the harder the train pulls + // away once the brake eases, so the band widens with the gradient (about 3 km/h at 20 per mille) + // an electro-pneumatic or electric brake responds at once and needs far less room + auto const holdspeed { holdlimit - ( + BrakeSystem == TBrakeSystem::Pneumatic ? + std::clamp( fAccGravity * 15.0, 1.0, 3.0 ) : + std::clamp( fAccGravity * 7.5, 1.0, 1.5 ) ) }; + if( ( speedestimate > VelDesired ) + && ( VelDesired == 0.0 ) ) { + // jesli stoj, to hamuj, ale i tak juz za pozno :) + AccDesired = std::min( AccDesired, -0.85 ); // hamuj solidnie } else { - // stop accelerating when close enough to target speed + // one continuous law on both sides of the target, proportional to the predicted + // difference: a demand that jumps to the braking threshold above the target and to + // full acceleration below it alternates, with a brake that responds in seconds, + // between a hard application and a full release AccDesired = std::min( AccDesired, // but don't override decceleration for VelNext - std::lerp( // ease off as you close to the target velocity - EU07_AI_NOACCELERATION, AccPreferred, - std::clamp( VelDesired - speedestimate, 0.0, fVelMinus ) / fVelMinus ) ); + std::clamp( + EU07_AI_NOACCELERATION - 0.04 * ( speedestimate - holdspeed ), + -0.05 + fAccThreshold, + std::max( EU07_AI_NOACCELERATION, AccPreferred ) ) ); + // HACK: for cargo trains with high braking threshold ensure we cross that threshold + if( true == IsCargoTrain + && fBrake_a0[0] > 0.2 + && speedestimate > holdlimit ) { + AccDesired -= std::clamp( fBrake_a0[ 0 ] - 0.2, 0.0, 0.15 ); + } } // final tweaks if( vel > EU07_AI_NOMOVEMENT ) { // going downhill also take into account impact of gravity AccDesired -= fAccGravity; + // from here on AccDesired is what the brake alone has to deliver, which is what + // IncBrake() sizes the handle position for; AbsAccS already contains the gravity, + // so the comparisons with it have to add this part back + fAccDesiredGravity = fAccGravity; // HACK: if the max allowed speed was exceeded something went wrong; brake harder AccDesired -= 0.15 * std::clamp( vel - VelDesired, 0.0, 5.0 ); } @@ -7773,6 +7815,7 @@ TController::adjust_desired_speed_for_braking_test() { } case 1: { AccDesired = EU07_AI_BRAKINGTESTACCELERATION; + fAccDesiredGravity = 0.0; VelDesired = DBT_VelocityBrake; if( ElapsedTime - DBT_BrakingTime > 1 ) { ForcePNBrake = true; @@ -7783,6 +7826,7 @@ TController::adjust_desired_speed_for_braking_test() { } case 2: { AccDesired = EU07_AI_BRAKINGTESTACCELERATION; + fAccDesiredGravity = 0.0; VelDesired = DBT_VelocityBrake; if( ElapsedTime - DBT_BrakingTime > 2 ) { DBT_BrakingTime = ElapsedTime; @@ -7794,6 +7838,7 @@ TController::adjust_desired_speed_for_braking_test() { } case 3: { AccDesired = safe_clamp( -AbsAccS, fAccThreshold * 1.01, fAccThreshold * 1.21 ); + fAccDesiredGravity = 0.0; VelDesired = DBT_VelocityBrake; if( vel <= DBT_VelocityRelease ) { DynamicBrakeTest = 4; @@ -7980,6 +8025,10 @@ void TController::control_braking_force() { } } + // AbsAccS is the resultant acceleration, gravity included, so it is compared with the + // resultant the AI asks for, not with the brake-only demand AccDesired holds on a descent + auto const accdesiredresultant { AccDesired + fAccDesiredGravity }; + if( is_emu() && !ForcePNBrake ) { // właściwie, to warunek powinien być na działający EP // Ra: to dobrze hamuje EP w EZT @@ -7991,13 +8040,13 @@ void TController::control_braking_force() { auto const accmargin = AccMax > 1.1 * AccDesired && fAccGravity < 0.025 ? 0.05 : 0.0; if( AccDesired < accthreshold // jeśli hamować - u góry ustawia się hamowanie na fAccThreshold - && ( AbsAccS > AccDesired + accmargin + && ( AbsAccS > accdesiredresultant + accmargin || BrakeCtrlPosition < 0 ) ) { // hamować bardziej, gdy aktualne opóźnienie hamowania mniejsze niż (AccDesired) cue_action( driver_hint::brakingforceincrease ); } else if( OrderCurrentGet() != Disconnect ) { // przy odłączaniu nie zwalniamy tu hamulca - if( AbsAccS < AccDesired - 0.05 ) { + if( AbsAccS < accdesiredresultant - 0.05 ) { // jeśli opóźnienie większe od wymaganego (z histerezą) luzowanie, gdy za dużo // TBD: check if the condition isn't redundant with the DecBrake() code if( BrakeCtrlPosition >= 0 ) { @@ -8013,10 +8062,12 @@ void TController::control_braking_force() { } // type & dt_ezt else { // a stara wersja w miarę dobrze działa na składy wagonowe - if( ( AccDesired < fAccGravity - 0.1 && AbsAccS > AccDesired + fBrake_a1[0] ) // regular braking + if( ( accdesiredresultant < fAccGravity - 0.1 && AbsAccS > accdesiredresultant + fBrake_a1[0] ) // regular braking || ( fAccGravity < -0.05 && velocity < -0.1 ) ) { // also brake if uphill and slipping back // u góry ustawia się hamowanie na fAccThreshold if( fBrakeTime < 0.0 + // the size of the brake demand decides whether to skip the reaction time, so the brake-only part + // (on a descent a heavy train needs its brake built up at once, the gravity does not help it slow) || AccDesired < fAccGravity - 0.5 || BrakeCtrlPosition <= 0 ) { // jeśli upłynął czas reakcji hamulca, chyba że nagłe albo luzował @@ -8033,8 +8084,8 @@ void TController::control_braking_force() { * 0.5 ); // Ra: tymczasowo, bo przeżyna S1 } } - if ( AccDesired < fAccGravity - 0.05 - && AccDesired - fBrake_a1[0] * 0.51 - AbsAccS > 0.05 ) { + if ( accdesiredresultant < fAccGravity - 0.05 + && accdesiredresultant - fBrake_a1[0] * 0.51 - AbsAccS > 0.05 ) { // jak hamuje, to nie tykaj kranu za często // yB: luzuje hamulec dopiero przy różnicy opóźnień rzędu 0.2 if( OrderCurrentGet() != Disconnect ) { // przy odłączaniu nie zwalniamy tu hamulca diff --git a/vehicle/Driver.h b/vehicle/Driver.h index e7c4fac00..c493afc8a 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -387,6 +387,7 @@ private: double AccDesired = 0.0; // przyspieszenie, jakie ma utrzymywać (<0:nie przyspieszaj,<-0.1:hamuj) 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 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 double VelLimit = -1.0; // predkość zadawana przez event jednokierunkowego ograniczenia prędkości // -1: brak ograniczenia prędkości From ce87aa0e090d4b5fba7042588df6e5e71895d5a1 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 28 Sep 2026 15:45:10 +0200 Subject: [PATCH 03/13] AI braking: plan approaches from the consist brake, stop 8-15 m short The new braking curve below plans the stop at the signal itself unless the stopping point moves with it (in testing, trains stopped 0-3 m past the signal), so both go in one change. 1. The braking curve. The braking curve towards a lower speed or a stop assumed a constant deceleration from the present speed over the whole distance. A pneumatic brake does not work like that: - it acts on the state the train is in once it has applied; while the train is still speeding up (downhill), plan from the speed it will have reached by then, over the distance left by then; - cast iron blocks grip harder as the train slows, so a constant deceleration plan asks for too much at the start and then releases; follow the shape of the consist brake table (fBrake_a0/a1) over speed instead (shaped_braking_demand), over the distance left once the brake has had its fill time; - no power while the train is still above the speed it is braking for. IncBrake() only adds braking when the brake table says the present position is not enough. When the table overestimates the consist (see the previous rapid step fix, or a descent), the train keeps slowing too little with the handle held. After the full fill time with the handle unchanged, believe the train rather than the table (fBrakeHeldTime). When the AI asks for no braking and the train is still slowing down more than that, release. With the gentler plan above the demand eases off early, and without this a heavy freight train in G came to a stand a few hundred metres before a W4 with its brake still on (seen in testing). 2. The stopping window. In train running the final approach to a stop signal began at fMaxProximityDist (15-40 m, cargo 25-50 m), aimed at half of fMinProximityDist (10-15 m, cargo 20-25 m), and set VelDesired = 0, which asks for hard braking whatever the distance left. Where a train came to a stand depended on how it entered that phase: on the test sceneries anywhere from 1 to 45 m short of the signal on the flat and 25-42 m uphill for heavy freight. For train running only (a W4 stop point, shunting and coupling keep the old margins and the old final approach): - stop 8-15 m short of the signal whatever the length of the train, aim at the middle of that window (stopping_margin) both with the braking curve and in the final approach, and ask there only for what the distance left takes, following the consist brake table; - start the final approach 50 m out (cargo 60 m) whether the train is moving or standing; it used to be 15-40 m while moving and 50 m once stopped, and with the gentler final approach above a train that came to a stand between the two released to creep closer and, uphill, rolled back and tried again; - uphill, while the gradient alone would bring the train to a stand short of the window, keep it rolling at no more than the speed the gradient can take off by then, with power if needed; - uphill, put the train brake on as the train comes to a stand, keep it on while it stands, and apply it for real if the train rolls back. This is done at the very end of pick_optimal_speed(): the adjustments made before it assume a train moving forward and can raise the demand back to "no braking" (with the rule placed earlier, EMUs rolled back up to 90 m in testing). The EMU branch of control_braking_force() gets the "slipping back" condition the wagon branch has always had. 3. Known limitation. Heavy trains of wagons with the ESt3AL2 distributor (413S, 601W and alike) respond more slowly than the AI expects: with the present brake model the distributor fills the AL2 relay chamber, whose capacity is set to 2 in TNESt3::SetSize (Przekladniki[i]->CreateCap(i)), through orifices sized for about 1 l, so these wagons need 34 s to reach 95 % cylinder pressure in G against 21 s for a plain ESt3. On a 20 per mille descent an ET22 with 20 loaded 413S wagons overran the stop signal by up to 6.5 m in 6 of 24 runs at x20 with this change. With the relay chamber at 1 l and the release orifices at about 80 % of their diameter, as the author of the brake model suggested, none of them did (0 of 16). --- vehicle/Driver.cpp | 217 ++++++++++++++++++++++++++++++++++++++++----- vehicle/Driver.h | 7 ++ 2 files changed, 200 insertions(+), 24 deletions(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 5b61b6dd1..09b607b69 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -3122,6 +3122,17 @@ 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 ] ) ) { + // 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 + OK = BrakeLevelAdd( BrakingLevelIncrease ); + } else OK = false; } @@ -3781,6 +3792,7 @@ void TController::BrakeLevelSet(double b) if (BrakeCtrlPosition == b) return; // nie przeliczać, jak nie ma zmiany BrakeCtrlPosition = std::clamp(b, (double)gbh_MIN, (double)gbh_MAX); + fBrakeHeldTime = 0.0; } bool TController::BrakeLevelAdd(double b) @@ -5911,6 +5923,7 @@ TController::update_timers( double dt ) { ElapsedTime += dt; WaitingTime += dt; fBrakeTime -= dt; // wpisana wartość jest zmniejszana do 0, gdy ujemna należy zmienić nastawę hamulca + fBrakeHeldTime += dt; if( mvOccupied->fBrakeCtrlPos != mvOccupied->Handle->GetPos( bh_FS ) ) { // brake charging timeout starts after charging ends BrakeChargingCooldown += dt; @@ -6659,12 +6672,27 @@ TController::determine_proximity_ranges() { // na jaka odleglosc i z jaka predkoscia ma podjechac do przeszkody // jeśli pociąg if( is_train() ) { - fMinProximityDist = std::clamp( 5 + iVehicles, 10, 15 ); - fMaxProximityDist = std::clamp( 10 + iVehicles, 15, 40 ); + if( true == IsAtPassengerStop ) { + // a stop point keeps the margins its W4 parameters (head or middle of the consist, + // platform length) were set up against + fMinProximityDist = std::clamp( 5 + iVehicles, 10, 15 ); + fMaxProximityDist = std::clamp( 10 + iVehicles, 15, 40 ); + if( IsCargoTrain ) { + fMinProximityDist += 10.0; + } + } + else { + // a train stops 8-15 m short of the signal whatever its length; closer than the lower + // edge it stops at once, the braking aims at the middle of the window, 3.5 m further out + fMinProximityDist = 8.0; + // the final approach begins far enough out to slow from the coasting speed down to the window, + // and at the same distance whether the train is moving or standing, so it does not flip + // between creeping closer and holding still when it comes to a stand short of the window + fMaxProximityDist = 50.0; + } if( IsCargoTrain ) { - // increase distances for cargo trains to take into account slower reaction to brakes - fMinProximityDist += 10.0; + // start the final approach earlier for cargo trains, their brakes react more slowly fMaxProximityDist += 10.0; /* if( IsHeavyCargoTrain ) { @@ -6683,7 +6711,7 @@ TController::determine_proximity_ranges() { if( mvOccupied->Vel < 0.1 ) { // jak stanie za daleko, to niech nie dociąga paru metrów - fMaxProximityDist = 50.0; + fMaxProximityDist = std::max( fMaxProximityDist, 50.0 ); } if( iDrivigFlags & moveLate ) { @@ -7314,6 +7342,26 @@ TController::pick_optimal_speed( double const Range ) { is_car() ? -2.0 : -0.9, is_car() ? 2.0 : 0.9 ); + if( ( true == is_train() ) + && ( ( OrderCurrentGet() & Obey_train ) != 0 ) + && ( VelNext == 0.0 ) + && ( ActualProximityDist <= fMaxProximityDist ) + && ( fAccGravity < -0.025 ) ) { + // uphill, the gradient alone brings a slow train to a stand within metres, far sooner than a released + // brake applies; so the train brake goes on while the train is coming to a stand, stays on while it + // stands, and is applied for real if the train has started to roll back. done last, so that none of + // the adjustments above, made for a train moving forward, can undo it + auto const vel { DirectionalVel() }; + if( std::abs( vel ) < EU07_AI_NOMOVEMENT ) { + VelDesired = 0.0; + AccDesired = std::min( AccDesired, EU07_AI_NOACCELERATION ); + } + else if( ( vel < 0.0 ) + || ( ( vel < 5.0 ) && ( AccDesired < 0.0 ) ) ) { + AccDesired = std::min( AccDesired, fAccGravity - 0.15 ); + } + } + // if the route ahead is blocked we might need to head the other way check_route_behind( 1000 ); // NOTE: legacy scan range value } @@ -7502,6 +7550,55 @@ brake_response_horizon( TMoverParameters const *Vehicle, TBrakeSystem const Brak return horizon; } +// distance [m] in which the gradient alone, with no brake and no power, brings a train rolling uphill at Velocity [km/h] to a stand +static double +uphill_stopping_distance( double const Velocity, double const Gravity ) { + return ( Velocity / 3.6 ) * ( Velocity / 3.6 ) / ( 2.0 * std::max( 0.01, -Gravity ) ); +} + +// speed [km/h] from which the gradient alone brings a train rolling uphill to a stand in Distance [m] +static double +uphill_coasting_speed( double const Distance, double const Gravity ) { + return 3.6 * std::sqrt( 2.0 * std::max( 0.01, -Gravity ) * std::max( 0.0, Distance ) ); +} + +// resultant acceleration [m/s2] to ask for now so that a steady application of the pneumatic brake takes the train +// from Velocity down to Target [km/h] over Distance [m], with the brake force following the speed the way the +// consist brake table says it does (cast iron blocks grip harder as the train slows) +static double +shaped_braking_demand( + double const Velocity, double const Target, double const Distance, double const Gravity, + double const Vmax, double const *Brakea0, double const *Brakea1, int const Tablesize ) { + + auto const fullbraking = [&]( double const Kmh ) { + auto const index { std::clamp( static_cast( Tablesize * Kmh / std::max( 1.0, Vmax ) ), 1, Tablesize ) }; + return std::max( 0.01, Brakea0[ index ] + 12.0 * Brakea1[ index ] ); }; + auto const travel = [&]( double const Ratio ) { + auto const steps { 24 }; + auto const step { ( Velocity - Target ) / steps }; + auto distance { 0.0 }; + for( auto i { 0 }; i < steps; ++i ) { + auto const kmh { Target + ( i + 0.5 ) * step }; + auto const deceleration { Ratio * fullbraking( kmh ) - Gravity }; + if( deceleration <= 0.001 ) { + return std::numeric_limits::max(); + } + distance += ( kmh / 3.6 ) * ( step / 3.6 ) / deceleration; + } + return distance; }; + + auto low { 0.0 }; + auto high { 3.0 }; + if( travel( high ) > Distance ) { + return Gravity - high * fullbraking( Velocity ); + } + for( auto i { 0 }; i < 30; ++i ) { + auto const middle { 0.5 * ( low + high ) }; + ( travel( middle ) > Distance ? low : high ) = middle; + } + return Gravity - high * fullbraking( Velocity ); +} + void TController::adjust_desired_speed_for_target_speed( double const Range ) { // ustalanie zadanego przyspieszenia @@ -7552,9 +7649,33 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) { } else { // hamowanie tak, aby stanąć - VelDesired = VelNext; - AccDesired = ( VelNext * VelNext - vel * vel ) / ( 25.92 * ( ActualProximityDist + 0.1 - 0.5*fMinProximityDist ) ); - AccDesired = std::min( AccDesired, fAccThreshold ); + if( ( OrderCurrentGet() & Obey_train ) == 0 ) { + VelDesired = VelNext; + AccDesired = ( VelNext * VelNext - vel * vel ) / ( 25.92 * ( ActualProximityDist + 0.1 - 0.5*fMinProximityDist ) ); + AccDesired = std::min( AccDesired, fAccThreshold ); + } + else { + // a train keeps its desired speed until the lower edge of the stopping window (a desired + // speed of 0 here asks for hard braking whatever the distance left), aims at the middle + // of the window, and asks only for what that takes + auto const stoppingroom { std::max( 0.5, ActualProximityDist - stopping_margin() ) }; + AccDesired = ( + BrakeSystem == TBrakeSystem::Pneumatic ? + // cast iron blocks grip harder as the train comes to a stand + shaped_braking_demand( + vel, VelNext, stoppingroom, fAccGravity, + mvOccupied->Vmax, fBrake_a0, fBrake_a1, BrakeAccTableSize ) : + ( VelNext * VelNext - vel * vel ) / ( 25.92 * stoppingroom ) ); + if( ( fAccGravity < -0.025 ) + && ( AccDesired >= fAccGravity - 0.001 ) // no brake needed at all + && ( ActualProximityDist - uphill_stopping_distance( vel, fAccGravity ) > stopping_margin() ) ) { + // uphill the gradient alone would bring the train to a stand short of the + // middle of the stopping window; keep it rolling at the present speed until that changes, + // but no faster than the gradient alone can take off by the middle of the window + AccDesired = std::min( 0.0, AccPreferred ); + VelDesired = min_speed( VelDesired, uphill_coasting_speed( ActualProximityDist - stopping_margin(), fAccGravity ) ); + } + } } } else { @@ -7578,17 +7699,57 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) { if( brakingdistance + std::max(slowdowndistance, fMaxProximityDist) >= ActualProximityDist - fMaxProximityDist ) { // don't slow down prematurely; as long as we have room to come to a full stop at a safe distance, we're good // ensure some minimal coasting speed, otherwise a vehicle entering this zone at very low speed will be crawling forever - auto const brakingpointoffset = VelNext * braking_distance_multiplier( VelNext ); - AccDesired = std::min( - AccDesired, - ( VelNext * VelNext - vel * vel ) - / ( 25.92 - * std::max( - ActualProximityDist - brakingpointoffset, - std::min( - ActualProximityDist, - brakingpointoffset ) ) - + 0.1 ) ); // najpierw hamuje mocniej, potem zluzuje + auto const brakingpointoffset = ( + ( VelNext == 0.0 ) && is_train() && ( OrderCurrentGet() & Obey_train ) ? + stopping_margin() : // the same stopping point as the final approach + VelNext * braking_distance_multiplier( VelNext ) ); + // a pneumatic brake acts on the state the train will be in once it has responded; while the + // train is still speeding up, plan the deceleration from there, so more is asked for at once + auto velocity { vel }; + auto distance { ActualProximityDist }; + if( ( BrakeSystem == TBrakeSystem::Pneumatic ) + && ( AbsAccS > 0.0 ) ) { + auto const horizon { brake_response_horizon( mvOccupied, BrakeSystem, AbsAccS, fBrake_a0[ 0 ] ) }; + velocity = vel + 3.6 * horizon * AbsAccS; + distance = std::max( 0.5 * ActualProximityDist, ActualProximityDist - ( vel + velocity ) * 0.5 / 3.6 * horizon ); + } + auto const brakingroom { + std::max( + distance - brakingpointoffset, + std::min( + distance, + brakingpointoffset ) ) }; + auto deceleration { ( VelNext * VelNext - velocity * velocity ) / ( 25.92 * brakingroom + 0.1 ) }; + if( ( BrakeSystem == TBrakeSystem::Pneumatic ) && ( velocity > VelNext ) ) { + // follow the shape of the consist brake force over speed rather than assume a constant + // deceleration, over the distance left once the brake has had time to fill (while the + // 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 + room = std::max( + 0.5 * brakingroom, + brakingroom - vel / 3.6 * brake_response_horizon( mvOccupied, BrakeSystem, 1.0, fBrake_a0[ 0 ] ) ); + } + deceleration = shaped_braking_demand( + velocity, VelNext, room + 0.1 / 25.92, fAccGravity, + mvOccupied->Vmax, fBrake_a0, fBrake_a1, BrakeAccTableSize ); + } + AccDesired = std::min( AccDesired, deceleration ); // najpierw hamuje mocniej, potem zluzuje + if( vel > VelNext ) { + if( deceleration < fAccGravity - 0.001 ) { + // no power while still above the speed being braked for + AccDesired = std::min( AccDesired, EU07_AI_NOACCELERATION ); + } + else if( ( fAccGravity < -0.025 ) + && ( ( OrderCurrentGet() & Obey_train ) != 0 ) + && ( ActualProximityDist - uphill_stopping_distance( vel, fAccGravity ) > brakingpointoffset ) ) { + // uphill the gradient alone slows the train down more than it is asked to; + // keep it rolling at the present speed until that changes + AccDesired = std::min( 0.0, AccPreferred ); + VelDesired = min_speed( VelDesired, uphill_coasting_speed( ActualProximityDist - brakingpointoffset, fAccGravity ) ); + } + } } } } @@ -7959,7 +8120,13 @@ void TController::control_tractive_force() { if( ActualProximityDist > (is_car() ? fMinProximityDist : // cars are allowed to move within min proximity distance fMaxProximityDist) ? // other vehicle types keep wider margin true : - velocity + 1.0 < VelNext ) { + ( velocity + 1.0 < VelNext ) + // uphill, a train told to hold its speed on the way to a stop needs the power to do it + || ( ( VelNext == 0.0 ) + && ( ( OrderCurrentGet() & Obey_train ) != 0 ) + && ( AccDesired >= 0.0 ) + && ( fAccGravity < -0.025 ) + && ( ActualProximityDist > fMinProximityDist ) ) ) { // ...to można przyspieszyć increase_tractive_force(); } @@ -8039,9 +8206,10 @@ void TController::control_braking_force() { auto const AccMax{ std::min(fBrake_a0[0] + 12 * fBrake_a1[0], mvOccupied->MED_amax) }; auto const accmargin = AccMax > 1.1 * AccDesired && fAccGravity < 0.025 ? 0.05 : 0.0; - if( AccDesired < accthreshold // jeśli hamować - u góry ustawia się hamowanie na fAccThreshold - && ( AbsAccS > accdesiredresultant + accmargin - || BrakeCtrlPosition < 0 ) ) { + if( ( AccDesired < accthreshold // jeśli hamować - u góry ustawia się hamowanie na fAccThreshold + && ( AbsAccS > accdesiredresultant + accmargin + || BrakeCtrlPosition < 0 ) ) + || ( fAccGravity < -0.05 && velocity < -0.1 && AccDesired < 0.0 ) ) { // also brake if uphill and slipping back // hamować bardziej, gdy aktualne opóźnienie hamowania mniejsze niż (AccDesired) cue_action( driver_hint::brakingforceincrease ); } @@ -8084,7 +8252,8 @@ void TController::control_braking_force() { * 0.5 ); // Ra: tymczasowo, bo przeżyna S1 } } - if ( accdesiredresultant < fAccGravity - 0.05 + 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 ) { // jak hamuje, to nie tykaj kranu za często // yB: luzuje hamulec dopiero przy różnicy opóźnień rzędu 0.2 diff --git a/vehicle/Driver.h b/vehicle/Driver.h index c493afc8a..0e65a4b94 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -353,6 +353,12 @@ private: void control_tractive_force(); void increase_tractive_force(); void control_braking_force(); + // how far short of a stop the AI aims: in train running the middle of the 8-15 m window before a signal, + // otherwise (a W4 stop point, shunting, coupling) half the minimum proximity distance, as before + double stopping_margin() const { + return ( ( OrderCurrentGet() & Obey_train ) != 0 ) && ( false == IsAtPassengerStop ) ? + fMinProximityDist + 3.5 : + 0.5 * fMinProximityDist; } void apply_independent_brake_only(); void check_route_ahead( double const Range ); void check_route_behind( double const Range ); @@ -387,6 +393,7 @@ private: double AccDesired = 0.0; // przyspieszenie, jakie ma utrzymywać (<0:nie przyspieszaj,<-0.1:hamuj) 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 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 From 4f2dfd93186f218f20c499ef20572b9fa5b30b83 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Tue, 29 Sep 2026 09:47:51 +0200 Subject: [PATCH 04/13] Drop redundant comparisons with true/false In the conditions this PR adds; no change in behaviour. --- McZapkie/Oerlikon_ESt.cpp | 2 +- vehicle/Driver.cpp | 6 +++--- vehicle/Driver.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/McZapkie/Oerlikon_ESt.cpp b/McZapkie/Oerlikon_ESt.cpp index d8d5b3da8..8705000a4 100644 --- a/McZapkie/Oerlikon_ESt.cpp +++ b/McZapkie/Oerlikon_ESt.cpp @@ -327,7 +327,7 @@ void TNESt3::Init( double const PP, double const HPP, double const LPP, double c double TNESt3::GetRapidRatio(double const Vel) const { // ESt3 has no rapid relay, and with disc brakes the rapid step stays engaged at all speeds - if( ( PrzekRapid == nullptr ) || ( true == RapidStaly ) ) { + if( ( PrzekRapid == nullptr ) || RapidStaly ) { return 1.0; } // a braking vehicle comes from above, so the step is still engaged down to the speed diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 09b607b69..d75500dbe 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -6672,7 +6672,7 @@ TController::determine_proximity_ranges() { // na jaka odleglosc i z jaka predkoscia ma podjechac do przeszkody // jeśli pociąg if( is_train() ) { - if( true == IsAtPassengerStop ) { + if( IsAtPassengerStop ) { // a stop point keeps the margins its W4 parameters (head or middle of the consist, // platform length) were set up against fMinProximityDist = std::clamp( 5 + iVehicles, 10, 15 ); @@ -7342,7 +7342,7 @@ TController::pick_optimal_speed( double const Range ) { is_car() ? -2.0 : -0.9, is_car() ? 2.0 : 0.9 ); - if( ( true == is_train() ) + if( is_train() && ( ( OrderCurrentGet() & Obey_train ) != 0 ) && ( VelNext == 0.0 ) && ( ActualProximityDist <= fMaxProximityDist ) @@ -7872,7 +7872,7 @@ TController::adjust_desired_speed_for_current_speed() { -0.05 + fAccThreshold, std::max( EU07_AI_NOACCELERATION, AccPreferred ) ) ); // HACK: for cargo trains with high braking threshold ensure we cross that threshold - if( true == IsCargoTrain + if( IsCargoTrain && fBrake_a0[0] > 0.2 && speedestimate > holdlimit ) { AccDesired -= std::clamp( fBrake_a0[ 0 ] - 0.2, 0.0, 0.15 ); diff --git a/vehicle/Driver.h b/vehicle/Driver.h index 0e65a4b94..fefa950ca 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -356,7 +356,7 @@ private: // how far short of a stop the AI aims: in train running the middle of the 8-15 m window before a signal, // otherwise (a W4 stop point, shunting, coupling) half the minimum proximity distance, as before double stopping_margin() const { - return ( ( OrderCurrentGet() & Obey_train ) != 0 ) && ( false == IsAtPassengerStop ) ? + return ( ( OrderCurrentGet() & Obey_train ) != 0 ) && !IsAtPassengerStop ? fMinProximityDist + 3.5 : 0.5 * fMinProximityDist; } void apply_independent_brake_only(); From 504d69b26a1be53aa4e6121685eda395ff10e47c Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Sat, 10 Oct 2026 23:39:19 +0200 Subject: [PATCH 05/13] AI braking: keep braking for a lower speed inside the proximity range Inside the proximity range only a stop had a final approach of its own; for a lower speed ahead that branch set nothing, so AccDesired stayed at the preferred acceleration. With the range at 50 m, which this PR uses so that trains stop 8-15 m short of a signal, the AI stopped braking within the last 50 m before a speed restriction and even took power: EMUs reached a 20 km/h restriction at 25 km/h. On master the range is 15-40 m, which kept this mostly out of sight. A lower speed ahead now keeps the braking curve right up to where it begins. Stops are unchanged. Measured in real time on current master (with #199), with a 20 km/h restriction one coach long, two runs of 32 consists each: over 22 km/h in the restriction on the flat 9 on master, 15 before this change, 1 after (a 36WEa at 22.1 km/h); on the 20 per mille descent 15, 0 and 0. Stops 8-15 m short of the signal stay as they were (flat 57 and 55 of 64, descent 44 and 42), no overruns. --- vehicle/Driver.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index c0e78b7cd..a5272b392 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -7518,7 +7518,10 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) { } else if (ActualProximityDist > fMinProximityDist) { // jedzie szybciej, niż trzeba na końcu ActualProximityDist, ale jeszcze jest daleko - if (ActualProximityDist < fMaxProximityDist) { + // only a stop has a final approach of its own inside the proximity range; a lower speed ahead + // keeps the braking curve below right up to where it begins, or the train would stop braking + // (and even take power) that far out + if( ( VelNext == 0.0 ) && ( ActualProximityDist < fMaxProximityDist ) ) { // jak minął już maksymalny dystans po prostu hamuj (niski stopień) // ma stanąć, a jest w drodze hamowania albo ma jechać /* From 6b82f1471aaf38b7417f28c018aa3e1e958416fc Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 2 Nov 2026 12:36:11 +0100 Subject: [PATCH 06/13] AI braking: take power off when holding the speed uphill near a stop Within fMaxProximityDist of a stop, going uphill, the AI holds its speed with AccDesired = min(0, AccPreferred). control_tractive_force only cut the power uphill when AccDesired < 0, so an AccDesired of exactly 0 kept the controller where it was. A consist that came into that range under power kept full power and crept up to the signal. Holding the speed uphill on the way to a stop now asks for no speeding up: power comes off once the train is faster than AccDesired allows. With the current brake valves this rarely shows: on a 10 per mille climb, four real-time runs of 32 consists each, the E186 with 25 empty 408W drew power in 0-14 samples of the last 50 m before and 0-8 after, no overruns either way. It surfaced once the AI predicted the ESt3 brake closer to what the valve delivers, where the same train crept to 12 km/h from 40 m out and overran the signal; with this change it stopped short in all runs. --- vehicle/Driver.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 2c59ddcda..436cdb7f6 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -8099,7 +8099,9 @@ void TController::control_tractive_force() { } else if( velocity > VelDesired + SpeedCtrlMargin || ( fAccGravity < -0.01 ? - AccDesired < 0.0 : + AccDesired < 0.0 + // holding the speed uphill on the way to a stop asks for no speeding up + || ( VelNext == 0.0 && ActualProximityDist <= fMaxProximityDist && AbsAccS > AccDesired + 0.05 ) : AbsAccS > AccDesired + 10.05 ) || IsAnyCouplerStretched ) { // jak za bardzo przyspiesza albo prędkość przekroczona From 1f3cf4fe56ee33fe38accd7903eaee401cd2d2a7 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 2 Nov 2026 16:02:06 +0100 Subject: [PATCH 07/13] AI braking: don't let the running brake test override a stop ahead adjust_desired_speed_for_braking_test runs after the target speed was worked out and overwrites AccDesired. In its third phase it holds the deceleration near fAccThreshold until the speed has dropped by 8 km/h. On a 20 per mille descent that application barely holds the speed, so the phase can last minutes, and a stop signal coming up meanwhile is ignored. In one real-time run an E186 with 19 container wagons started the test 1290 m before a stop signal, lost 7 km/h by the signal and overran it by 274 m after the emergency application. While the test runs the AI now uses whichever of the two asks for the harder braking: the test may brake harder than the route needs, never softer. Measured in real time, four runs of 32 consists each on a 20 per mille descent, on the flat and on the descent with half the rail adhesion: the test no longer runs within 600 m of a stop signal (before: down to 414 m on this branch, and 14 m with the overrun above in a build with other brake changes), no overruns, stops 8-15 m short of the signal 86, 117 and 83 of 128 (before 90, 117 and 78). --- vehicle/Driver.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 436cdb7f6..6d6c1532e 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -7927,6 +7927,10 @@ TController::adjust_desired_speed_for_braking_test() { if( false == AIControllFlag ) { return; } // TBD: add notification about braking test and enable it for human driver as well? auto const vel { DirectionalVel() }; + // what the route ahead asks for; the test may brake harder than that, never softer + auto const routeaccdesired { AccDesired }; + auto const routeaccdesiredgravity { fAccDesiredGravity }; + auto const routeveldesired { VelDesired }; switch( DynamicBrakeTest ) { case 0: { @@ -7993,6 +7997,14 @@ TController::adjust_desired_speed_for_braking_test() { break; } } + // a stop or a restriction coming up while the test runs takes precedence + if( ( DynamicBrakeTest >= 1 ) + && ( DynamicBrakeTest <= 3 ) + && ( routeaccdesired < AccDesired ) ) { + AccDesired = routeaccdesired; + fAccDesiredGravity = routeaccdesiredgravity; + VelDesired = std::min( VelDesired, routeveldesired ); + } } void From 18bc7e06bfe58a4efff593dc3245a27289605ed3 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 2 Nov 2026 16:43:16 +0100 Subject: [PATCH 08/13] AI braking: move the uphill power-off check into its own function The condition added for holding the speed uphill near a stop pushed the cognitive complexity of control_tractive_force() to 26, over the 25 SonarCloud allows. It now lives in is_too_fast_uphill(); the behaviour is unchanged. --- vehicle/Driver.cpp | 13 ++++++++----- vehicle/Driver.h | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 6d6c1532e..2311ffacf 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -8110,11 +8110,7 @@ void TController::control_tractive_force() { cue_action( driver_hint::mastercontrollersetzerospeed ); } else if( velocity > VelDesired + SpeedCtrlMargin - || ( fAccGravity < -0.01 ? - AccDesired < 0.0 - // holding the speed uphill on the way to a stop asks for no speeding up - || ( VelNext == 0.0 && ActualProximityDist <= fMaxProximityDist && AbsAccS > AccDesired + 0.05 ) : - AbsAccS > AccDesired + 10.05 ) + || ( fAccGravity < -0.01 ? is_too_fast_uphill() : AbsAccS > AccDesired + 10.05 ) || IsAnyCouplerStretched ) { // jak za bardzo przyspiesza albo prędkość przekroczona // dodany wyjatek na "pelna w przod" @@ -8126,6 +8122,13 @@ void TController::control_tractive_force() { SpeedSet(); // ciągla regulacja prędkości } +bool TController::is_too_fast_uphill() const { + + if( AccDesired < 0.0 ) { return true; } + // holding the speed uphill on the way to a stop asks for no speeding up + return ( VelNext == 0.0 && ActualProximityDist <= fMaxProximityDist && AbsAccS > AccDesired + 0.05 ); +} + void TController::increase_tractive_force() { if( fActionTime < 0.0 ) { return; } // gdy jest nakaz poczekać z jazdą, to nie ruszać diff --git a/vehicle/Driver.h b/vehicle/Driver.h index af97b4a1e..11a01517f 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -358,6 +358,7 @@ private: void control_motor_connectors(); void control_tractive_force(); void increase_tractive_force(); + bool is_too_fast_uphill() const; void control_braking_force(); // how far short of a stop the AI aims: in train running the middle of the 8-15 m window before a signal, // otherwise (a W4 stop point, shunting, coupling) half the minimum proximity distance, as before From bb43f559e8ede590142d5383e4686e8d3d563f6c Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 2 Nov 2026 17:01:21 +0100 Subject: [PATCH 09/13] ESt3: spring preload, minimum stage, releaser time and AL2 timing Corrections to the TNESt3 distributor, checked against the ESt3 data and acceptance requirements (orange book tables 3-5, 3-7 and 3-9) and discussed with youBy: - The main control device settles at (control reservoir - pipe - 0.21) times the ratio instead of using a 0.05 bar offset. The 0.21 bar is the spring preload; without it the first stage came 0.2 bar of pipe drop too early. BVM is derived from the same offset, so a full service drop still gives MaxBP. - When the valve brakes it gives at least the minimum stage of 0.28 bar (acceptance tests 12 and 22 require 0.25-0.30 bar); when the target drops to zero the cylinder vents completely. - The automatic releaser vents the 15 l control reservoir from 5.0 to 0.4 bar in about 23 s (test 27: 23 +/- 3 s) instead of 5.6 s. - 12" cylinder fill nozzle dTN is 1.62, not 1.65 (typo). - With the AL2 relay the impulse chamber is 1 l and the cylinder nozzles are scaled so that the fill and release times meet table 3-9; before, the AL2 timings had been set by eye. On a valve test bench running the acceptance procedures, the number of results out of tolerance drops from 77 to 40. The remaining ones need parts that are not modelled yet (accelerator venting, ZP/ZS filling, intermediate chamber). --- McZapkie/Oerlikon_ESt.cpp | 44 +++++++++++++++++++++++++++++---------- McZapkie/Oerlikon_ESt.h | 1 + 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/McZapkie/Oerlikon_ESt.cpp b/McZapkie/Oerlikon_ESt.cpp index eb3a9ad4b..f37dc69f9 100644 --- a/McZapkie/Oerlikon_ESt.cpp +++ b/McZapkie/Oerlikon_ESt.cpp @@ -16,6 +16,11 @@ Copyright (C) 2007-2014 Maciej Cierniak #include "Oerlikon_ESt.h" #include "utilities/utilities.h" +// wstepny nacisk sprezyn na glowny przyrzad rozrzadczy, wyrazony jako roznica cisnien ZS - PG [bar] +static double const dPSprezyny = 0.21; +// najmniejszy stopien hamowania w cylindrze [bar] (tablica 3-9, proby 12 i 22: 0,25-0,30) +static double const pStopnia = 0.28; + double d2A( double const d ) { return d * d * 0.7854 / 1000.0; @@ -218,10 +223,11 @@ double TNESt3::GetPF( double const PP, double const dt, double const Vel ) // pr CheckReleaser(dt); // luzowanie + double const cel{ CelBCP( CVP, VVP ) }; if ((BrakeStatus & b_hld) == b_off) dV = PF(0.0, BCP, Nozzles[dTO] * nastG + (1.0 - nastG) * Nozzles[dOO]) - * dt * (0.1 + 4.9 * std::min(0.2, BCP - ((CVP - 0.05 - VVP) * BVM + 0.1))); + * dt * (0.1 + 4.9 * std::min(0.2, BCP - (cel > 0.0 ? cel + 0.1 : 0.0))); // bez stopnia luzuje do zera else dV = 0.0; // BrakeCyl.Flow(-dV); @@ -229,7 +235,7 @@ double TNESt3::GetPF( double const PP, double const dt, double const Vel ) // pr if ( (BrakeStatus & b_on) == b_on && Przekladniki[1]->P() * HBG300 < MaxBP ) dV = PF( BVP, BCP, Nozzles[dTN] * (nastG + 2.0 * (BCP < Podskok ? 1.0 : 0.0)) + Nozzles[dON] * (1.0 - nastG) ) - * dt * (0.1 + 4.9 * std::min( 0.2, (CVP - 0.05 - VVP) * BVM - BCP ) ); + * dt * (0.1 + 4.9 * std::min( 0.2, cel - BCP ) ); else dV = 0; // BrakeCyl.Flow(-dV); @@ -313,7 +319,7 @@ void TNESt3::Init( double const PP, double const HPP, double const LPP, double c Miedzypoj->CreateCap(5.0); Miedzypoj->CreatePress(PP); - BVM = 1.0 / (HPP - 0.05 - LPP) * MaxBP; + BVM = 1.0 / (HPP - dPSprezyny - LPP) * MaxBP; BrakeDelayFlag = BDF; @@ -345,6 +351,13 @@ double TNESt3::GetCRP() // return Miedzypoj.P; } +double TNESt3::CelBCP( double const CVP, double const VVP ) const // docelowe cisnienie w cylindrze +{ + double const cel{ ( CVP - dPSprezyny - VVP ) * BVM }; + // gdy zawor hamuje, nie daje mniej niz najmniejszy stopien + return ( cel > 0.0 ? std::max( cel, pStopnia ) : cel ); +} + void TNESt3::CheckState(double const BCP, double & /*dV1*/) // glowny przyrzad rozrzadczy { double const VVP{ ValveRes->P() }; @@ -355,14 +368,14 @@ void TNESt3::CheckState(double const BCP, double & /*dV1*/) // glowny przyrzad r Przys_blok = false; // sprawdzanie stanu - // if ((BrakeStatus and 1)=1)and(BCP>0.25)then - if (VVP + 0.01 + BCP / BVM < CVP - 0.05 && Przys_blok) + double const cel{ CelBCP( CVP, VVP ) }; + if (BCP < cel - 0.01 * BVM && Przys_blok) BrakeStatus |= b_on | b_hld; // hamowanie stopniowe; - else if (VVP - 0.01 + (BCP - 0.1) / BVM > CVP - 0.05) + else if (BCP - 0.1 > cel + 0.01 * BVM) BrakeStatus &= ~( b_on | b_hld ); // luzowanie; - else if (VVP + BCP / BVM > CVP - 0.05) + else if (BCP > cel) BrakeStatus &= ~b_on; // zatrzymanie napelaniania; - else if (VVP + (BCP - 0.1) / BVM < CVP - 0.05 && BCP > 0.25) // zatrzymanie luzowania + else if (BCP - 0.1 < cel && BCP > 0.25) // zatrzymanie luzowania BrakeStatus |= b_hld; if( ( BrakeStatus & b_hld ) == 0 ) @@ -391,7 +404,7 @@ void TNESt3::CheckReleaser(double const dt) // odluzniacz // odluzniacz automatyczny if ((BrakeStatus & b_rls) == b_rls) { - CntrlRes->Flow(PF(CVP, 0, 0.02) * dt); + CntrlRes->Flow(PF(CVP, 0, 0.0049) * dt); // ZS 15 l z 5,0 do 0,4 bar w 23 s (proba odbioru nr 27) if (CVP < VVP + 0.3 || false == autom) BrakeStatus &= ~b_rls; } @@ -560,7 +573,7 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie { // dON,dOO,dTN,dTO,dP,dS Nozzles[dON] = 3.7; Nozzles[dOO] = 2.50; - Nozzles[dTN] = 1.65; + Nozzles[dTN] = 1.62; Nozzles[dTO] = 1.39; Nozzles[dP] = 2.65; Nozzles[dPd] = 1.80; @@ -649,6 +662,14 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie Nozzles[dSd] = 1.1; Nozzles[dSm] = 0.9; + if( PrzekCiagly != nullptr ) { + // dysze cylindra skalibrowane na komore impulsowa przekladnika AL2 (1 l) wg czasow z tablicy 3-9 + Nozzles[dON] *= 0.914; + Nozzles[dTN] *= 0.885; + Nozzles[dOO] *= 0.82; + Nozzles[dTO] *= 0.8; + } + // przeliczanie z mm^2 na l/m for (auto &nozzle : Nozzles) { @@ -657,7 +678,8 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie for (int i = 1; i < 4; ++i) { Przekladniki[i]->BrakeRes = BrakeRes; - Przekladniki[i]->CreateCap(i); + // komora impulsowa przekladnika AL2 ma 1 l + Przekladniki[i]->CreateCap( ( Przekladniki[i] == PrzekCiagly ) ? 1.0 : i ); Przekladniki[i]->CreatePress(BrakeCyl->P()); if (i < 3) Przekladniki[i]->Next = Przekladniki[i + 1]; diff --git a/McZapkie/Oerlikon_ESt.h b/McZapkie/Oerlikon_ESt.h index 9cdffe7bc..01aefb7f2 100644 --- a/McZapkie/Oerlikon_ESt.h +++ b/McZapkie/Oerlikon_ESt.h @@ -196,6 +196,7 @@ class TNESt3 : public TBrake { void EStParams(double i_crc) const; // parametry charakterystyczne dla ESt double GetCRP() override; double GetRapidRatio(double const Vel) const override; + double CelBCP(double const CVP, double const VVP) const; // docelowe cisnienie w cylindrze void CheckState(double const BCP, double &dV1); // glowny przyrzad rozrzadczy void CheckReleaser(double const dt); // odluzniacz double CVs(double const BP) const; // napelniacz sterujacego From 6374e2d32e7ef00bb938d7b79763b1aae1d57779 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 2 Nov 2026 17:01:32 +0100 Subject: [PATCH 10/13] AI braking: predict the ESt3 brake force from the distributor characteristic --- McZapkie/Mover.cpp | 1 + McZapkie/Oerlikon_ESt.cpp | 11 +++++++++++ McZapkie/Oerlikon_ESt.h | 1 + McZapkie/hamulce.h | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 429c2df17..2b2275112 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -5091,6 +5091,7 @@ void TMoverParameters::ComputeTotalForce(double dt) double TMoverParameters::BrakeForceR(double ratio, double velocity) { double press = 0; + ratio = Hamulec->GetBrakeRatio(ratio); if (MBPM > 2) { press = MaxBrakePress[1] + (MaxBrakePress[3] - MaxBrakePress[1]) * std::min(1.0, (TotalMass - Mass) / (MBPM - Mass)); diff --git a/McZapkie/Oerlikon_ESt.cpp b/McZapkie/Oerlikon_ESt.cpp index f37dc69f9..1e7a5be66 100644 --- a/McZapkie/Oerlikon_ESt.cpp +++ b/McZapkie/Oerlikon_ESt.cpp @@ -351,6 +351,17 @@ double TNESt3::GetCRP() // return Miedzypoj.P; } +double TNESt3::GetBrakeRatio( double const Ratio ) const // udzial cisnienia w cylindrze dla udzialu spadku w PG +{ + if( MaxBP <= 0.0 || BVM <= 0.0 ) { + return Ratio; + } + // pelny spadek sluzbowy w PG (HPP - LPP), odtworzony z przelozenia + double const spadek{ MaxBP / BVM + dPSprezyny }; + double const cel{ ( Ratio * spadek - dPSprezyny ) * BVM }; + return ( cel > 0.0 ? std::max( cel, pStopnia ) / MaxBP : 0.0 ); +} + double TNESt3::CelBCP( double const CVP, double const VVP ) const // docelowe cisnienie w cylindrze { double const cel{ ( CVP - dPSprezyny - VVP ) * BVM }; diff --git a/McZapkie/Oerlikon_ESt.h b/McZapkie/Oerlikon_ESt.h index 01aefb7f2..02d65a285 100644 --- a/McZapkie/Oerlikon_ESt.h +++ b/McZapkie/Oerlikon_ESt.h @@ -197,6 +197,7 @@ class TNESt3 : public TBrake { double GetCRP() override; double GetRapidRatio(double const Vel) const override; double CelBCP(double const CVP, double const VVP) const; // docelowe cisnienie w cylindrze + double GetBrakeRatio(double const Ratio) const override; // udzial cisnienia w cylindrze dla udzialu spadku w PG void CheckState(double const BCP, double &dV1); // glowny przyrzad rozrzadczy void CheckReleaser(double const dt); // odluzniacz double CVs(double const BP) const; // napelniacz sterujacego diff --git a/McZapkie/hamulce.h b/McZapkie/hamulce.h index 5b5f8933c..37b9b6a4d 100644 --- a/McZapkie/hamulce.h +++ b/McZapkie/hamulce.h @@ -421,6 +421,10 @@ class TBrake double GetVRP() const; // cisnienie komory wstepnej rozdzielacza /// Returns control reservoir (ZS) pressure [bar]; defaults to the auxiliary reservoir for valves without a dedicated ZS. virtual double GetCRP(); // cisnienie zbiornika sterujacego + /// Share of the full cylinder pressure the distributor settles at for the given share + /// of the full service pipe drop. Used only to predict the brake force; the default is proportional. + /// Share of the full service brake pipe drop. + virtual double GetBrakeRatio(double const Ratio) const { return Ratio; } /// /// Sets the brake delay (G/P/R/M) selector if requested mode is supported. /// From 439e892abdaeac36bb76f3df2c8f926f76791477 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Sat, 7 Nov 2026 15:35:35 +0100 Subject: [PATCH 11/13] AI: use the locomotive releaser only when starting off The rule "wyluzuj lokomotywe, to szybciej ruszymy" released the locomotive's own brake whenever power was asked for and the locomotive cylinder held more than 0.4 bar. It had no speed condition, so it also fired in motion, at 50-75 km/h, right after an application. On a locomotive with the EStED distributor (E186) this vented the control reservoir; with the pipe not yet recharged the locomotive brake then faded to nothing at the next application. The rule is meant for starting off, so it now applies only below 1 km/h. --- vehicle/Driver.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 2311ffacf..8d2a3fac6 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -8292,8 +8292,11 @@ void TController::control_releaser() { } } // wyluzuj lokomotywę, to szybciej ruszymy + // this rule is for starting off, so only below 1 km/h: at speed, just after an application, it would vent + // the control reservoir of a locomotive that may have to brake again before the pipe has refilled it if( mvOccupied->BrakePress > 0.4 - && mvOccupied->Hamulec->GetCRP() > 4.9 ) { + && mvOccupied->Hamulec->GetCRP() > 4.9 + && mvOccupied->Vel < 1.0 ) { actuate = true; } // keep engine brakes released during coupling/uncoupling From 8d1d9eec9b2f2e0ae15dc20f21c943e7f9693c89 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Sat, 7 Nov 2026 18:10:47 +0100 Subject: [PATCH 12/13] 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 From 39244a88a500d51ada0f8c33ccc3056848466d22 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Sat, 7 Nov 2026 21:17:12 +0100 Subject: [PATCH 13/13] tests: wait for the pressure switches before notching up full_power() notched up as soon as the controlling half had closed its line contactors. In the ET42 test the locomotive brake is still at about 2.0 bar then, right at the pressure switch threshold, so whether the second half starts as well depended on thousandths of a bar in its cylinder: a half whose switch is on when the controller leaves zero doesn't close its line contactors until the controller is back at zero. Any change to the air brakes could tip it either way (with the corrected ESt3 the second half stayed off and the train reached 88.7 instead of 106 km/h). The helper now waits, as a driver does, until no pressure switch in the train cuts the start off. --- tests/unit/scenario.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/scenario.cpp b/tests/unit/scenario.cpp index b933a0ab5..4af8be18a 100644 --- a/tests/unit/scenario.cpp +++ b/tests/unit/scenario.cpp @@ -5,6 +5,21 @@ namespace test { +namespace { + +// whether the pressure switch of a vehicle in the train still cuts the start off: a half of a locomotive whose +// switch is on when the controller leaves zero doesn't close its line contactors until the controller is back at zero +bool start_cut_off(consist &Train) { + for (std::size_t i = 0; i < Train.size(); ++i) { + if (Train[i].mover().ControlPressureSwitch) { + return true; + } + } + return false; +} + +} // namespace + scenario::scenario(std::string Name, std::vector Channels, track const &Track) : m_name(std::move(Name)), m_channels(std::move(Channels)), m_track(Track) { reseed_random(); } @@ -53,6 +68,11 @@ void scenario::get_going() { void scenario::full_power(double Seconds) { auto &controlling{driver().controlling}; + // as a driver does, wait for the brakes to let go far enough for every pressure switch of the train to allow the + // start; otherwise whether all the halves of a locomotive start hangs on thousandths of a bar of their cylinders + for (int i = 0; i < 120 && start_cut_off(m_train); ++i) { + wait(1.0); + } controlling.IncMainCtrl(1); for (int i = 0; i < 90 && false == controlling.StLinFlag; ++i) { wait(1.0);