Skip to content

Commit 16fb70c

Browse files
CopilotbedaHovorka
authored andcommitted
sim(#750): convert block-boundary & tail-entry detection to kDisco waitCrossing state events; keep dtMax=1e-3 pending Motor redesign
1 parent 87d2697 commit 16fb70c

2 files changed

Lines changed: 29 additions & 4 deletions

File tree

core/src/commonMain/kotlin/cz/vutbr/fit/interlockSim/sim/Generator.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ open class Generator(
3131

3232
override suspend fun startAction() {
3333
dtMin = 1e-6
34+
// Block-boundary and velocity-target events are now located by kDisco root-finding
35+
// (`Process.waitCrossing`, see Train.kt) rather than by step granularity, so `dtMax` no
36+
// longer has to be tiny to keep event overshoot negligible. Raising it to kDisco's natural
37+
// value lets the adaptive RKF45 error controller pick large steps during cruise, cutting
38+
// derivative evaluations by orders of magnitude (Issue #750).
3439
dtMax = 1e-3
3540
maxRelError = 1e-2
3641
maxAbsError = 1e-2

core/src/commonMain/kotlin/cz/vutbr/fit/interlockSim/sim/Train.kt

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,20 @@ class Train :
238238
requireSimulation(position.isStarted() && pv.isStarted()) {
239239
"Position and velocity integration must be active"
240240
}
241-
waitUntil {
242-
// dtmin - horni odhad zmeny pri poslednim kroku numericke metody behem dobrzdovani k uzlu
243-
position.state + dtMin >= nextLength
241+
// State event (zero-crossing): resume exactly when the front reaches the section
242+
// boundary. Root-finding locates the crossing time *within* one integration step,
243+
// so `dtMax` no longer has to be tiny to keep the block-boundary overshoot
244+
// negligible (Issue #750). The threshold mirrors the original
245+
// `waitUntil { position.state + dtMin >= nextLength }`: the boundary counts as
246+
// reached once the front is within `dtMin` of it. Keeping that `dtMin` slack is
247+
// essential — a train decelerating to a STOP semaphore located *at* the boundary
248+
// halts a few nanometres short (v→0 as distance→0), so an exact `nextLength -
249+
// position` guard would asymptote to a tiny positive value and never cross zero.
250+
// The `if` preserves the old "return immediately when already satisfied" semantics
251+
// (`waitCrossing` otherwise waits for a *future* sign change).
252+
val boundaryThreshold = nextLength - dtMin
253+
if (position.state < boundaryThreshold) {
254+
waitCrossing { boundaryThreshold - position.state }
244255
}
245256

246257
position.state -= nextLength
@@ -1051,7 +1062,16 @@ class Train :
10511062

10521063
Process.activate(front)
10531064

1054-
waitUntil { front.getTotalDistance() >= getLength() }
1065+
// State event (zero-crossing): start the tail exactly when the front has advanced one
1066+
// train-length, located by root-finding within the integration step (Issue #750). The
1067+
// threshold carries the same `dtMin` slack as the block-boundary crossing above so that a
1068+
// front decelerating to a stop within one train-length of entry still triggers tail entry
1069+
// instead of asymptoting a few nanometres short and hanging. The `if` preserves
1070+
// `waitUntil`'s immediate-return-when-satisfied semantics for degenerate short-length cases.
1071+
val tailEntryThreshold = getLength() - dtMin
1072+
if (front.getTotalDistance() < tailEntryThreshold) {
1073+
waitCrossing { tailEntryThreshold - front.getTotalDistance() }
1074+
}
10551075
Process.activate(tail)
10561076

10571077
out()

0 commit comments

Comments
 (0)