From d983224203bbba477ab03c436bf9481a9445862d Mon Sep 17 00:00:00 2001 From: Eloi Charpentier Date: Thu, 20 Jun 2024 14:29:24 +0200 Subject: [PATCH] core: reduce path requirement to compute spacing resource uses We used to require path until it includes a zone that isn't necessary for the train. But we don't need to know which precise block it is, it doesn't need to be included in the path. --- .../conflicts/SpacingResourceGenerator.kt | 43 +++++++++++-------- .../fr/sncf/osrd/stdcm/graph/STDCMGraph.kt | 2 +- .../sncf/osrd/stdcm/graph/STDCMPathfinding.kt | 2 +- .../conflicts/SpacingResourceGeneratorTest.kt | 39 ++++++++++++++++- 4 files changed, 63 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/fr/sncf/osrd/conflicts/SpacingResourceGenerator.kt b/core/src/main/java/fr/sncf/osrd/conflicts/SpacingResourceGenerator.kt index 8e2d2574e83..5a28b03fe9f 100644 --- a/core/src/main/java/fr/sncf/osrd/conflicts/SpacingResourceGenerator.kt +++ b/core/src/main/java/fr/sncf/osrd/conflicts/SpacingResourceGenerator.kt @@ -218,13 +218,14 @@ class SpacingRequirementAutomaton( } // Returns true if the zone is required to be clear for the signal to not be constraining. - // Returns false if it's either not required, or we need a longer block path to tell. + // Returns false if it's not required + // Returns null if we need more path private fun isZoneIndexRequiredForSignal( probedZoneIndex: Int, pathSignal: PathSignal, routes: List, trainState: SignalingTrainState - ): Boolean { + ): Boolean? { val firstBlockIndex = pathSignal.minBlockPathIndex // List of blocks to include in the simulator call @@ -238,10 +239,10 @@ class SpacingRequirementAutomaton( var nSimulatedZones = blockInfra.getBlockPath(blocks[0]).size // Add blocks in the block path until the probed zone is covered - while (probedZoneIndex - firstSimulatedZone + 1 > nSimulatedZones) { + while (probedZoneIndex - firstSimulatedZone > nSimulatedZones) { if (firstBlockIndex + blocks.size >= incrementalPath.blockCount) { // exiting, the end of the block path has been reached - return false + return null } val newBlock = incrementalPath.getBlock(firstBlockIndex + blocks.size) blocks.add(newBlock) @@ -249,7 +250,9 @@ class SpacingRequirementAutomaton( } val zoneStates = MutableList(nSimulatedZones) { ZoneStatus.CLEAR } - zoneStates[probedZoneIndex - firstSimulatedZone] = ZoneStatus.OCCUPIED + if (probedZoneIndex - firstSimulatedZone < zoneStates.size) { + zoneStates[probedZoneIndex - firstSimulatedZone] = ZoneStatus.OCCUPIED + } // Otherwise we rely on the `followingZoneState` of `simulator.evaluate` val simulatedSignalStates = simulator.evaluate( rawInfra, @@ -271,12 +274,23 @@ class SpacingRequirementAutomaton( } // Returns the index of the first zone that isn't required for the given signal, - // or null if we need more path to determine it + // or null if we need more path to determine it. The returned value may be one index + // further than the end of the path, if we need the whole path but nothing more. private fun findFirstNonRequiredZoneIndex( pathSignal: PathSignal, routes: List, trainState: SignalingTrainState ): Int? { + // Check if more path is needed for a valid solution + // (i.e. the zone after the end of the path is required) + val lastZoneIndex = incrementalPath.getBlockEndZone(incrementalPath.blockCount - 1) + if ( + !incrementalPath.pathComplete && + isZoneIndexRequiredForSignal(lastZoneIndex, pathSignal, routes, trainState) != false + ) { + return null + } + // We are looking for the index `i` where `isZoneIndexRequiredForSignal` returns // true at `i-1` and false at `i`. We could just iterate starting at 0, but // because `i` is not that small (20 on average) and the signaling @@ -288,7 +302,7 @@ class SpacingRequirementAutomaton( // on start+20 that rarely exceeds start+40. // We run a binary search on that range, and iterate one by one when the solution is above. var lowerBound = getSignalProtectedZone(pathSignal) - val initialUpperBound = min(lowerBound + 40, incrementalPath.zonePathCount) + val initialUpperBound = min(lowerBound + 40, lastZoneIndex) var upperBound = initialUpperBound // Main loop, binary search @@ -296,7 +310,7 @@ class SpacingRequirementAutomaton( if (lowerBound == upperBound) break val probedZoneIndex = (upperBound + lowerBound) / 2 val required = - isZoneIndexRequiredForSignal(probedZoneIndex, pathSignal, routes, trainState) + isZoneIndexRequiredForSignal(probedZoneIndex, pathSignal, routes, trainState)!! if (required) { lowerBound = probedZoneIndex + 1 } else { @@ -306,18 +320,9 @@ class SpacingRequirementAutomaton( // Handle the case where the result is higher than the initial upper bound while ( - lowerBound >= initialUpperBound && - isZoneIndexRequiredForSignal(lowerBound, pathSignal, routes, trainState) + lowerBound in initialUpperBound ..< lastZoneIndex && + isZoneIndexRequiredForSignal(lowerBound, pathSignal, routes, trainState)!! ) lowerBound++ - - // Check if more path is needed for a valid solution - // (i.e. the first zone that is not required is out of bounds) - if ( - lowerBound >= incrementalPath.getBlockEndZone(incrementalPath.blockCount - 1) && - !incrementalPath.pathComplete - ) { - return null - } return lowerBound } diff --git a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMGraph.kt b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMGraph.kt index 0fda950e864..f1d98e0aa7a 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMGraph.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMGraph.kt @@ -96,7 +96,7 @@ class STDCMGraph( visitedNodes.markAsVisited(fingerprint, node.time) res.addAll(STDCMEdgeBuilder.fromNode(this, node, explorer).makeAllEdges()) } else { - val extended = extendLookaheadUntil(node.infraExplorer.clone(), 4) + val extended = extendLookaheadUntil(node.infraExplorer.clone(), 3) for (newPath in extended) { if (newPath.getLookahead().size == 0) continue newPath.moveForward() diff --git a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMPathfinding.kt b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMPathfinding.kt index 086c6e1593f..1ddee578374 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMPathfinding.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMPathfinding.kt @@ -252,7 +252,7 @@ class STDCMPathfinding( for (location in locations) { val infraExplorers = initInfraExplorerWithEnvelope(fullInfra, location, rollingStock, stops, constraints) - val extended = infraExplorers.flatMap { extendLookaheadUntil(it, 4) } + val extended = infraExplorers.flatMap { extendLookaheadUntil(it, 3) } for (explorer in extended) { val edges = STDCMEdgeBuilder(explorer, graph) diff --git a/core/src/test/java/fr/sncf/osrd/conflicts/SpacingResourceGeneratorTest.kt b/core/src/test/java/fr/sncf/osrd/conflicts/SpacingResourceGeneratorTest.kt index 3cd0fdde666..931f225ce66 100644 --- a/core/src/test/java/fr/sncf/osrd/conflicts/SpacingResourceGeneratorTest.kt +++ b/core/src/test/java/fr/sncf/osrd/conflicts/SpacingResourceGeneratorTest.kt @@ -146,10 +146,10 @@ class SpacingResourceGeneratorTest { res.add(iterationResult) } for (i in res.indices) { - // We need at least 4 blocks to find a block that doesn't restrict the signal at the end + // We need at least 3 blocks to find a block that doesn't restrict the signal at the end // of block 1 val nBlocks = i + 1 - val expectedNotEnoughPath = nBlocks < 4 + val expectedNotEnoughPath = nBlocks < 3 assertEquals(expectedNotEnoughPath, res[i] is NotEnoughPath) } } @@ -282,6 +282,41 @@ class SpacingResourceGeneratorTest { assertEquals(automaton.callbacks.currentTime, requirement.endTime) } } + + @Test + fun testRequiredPathLength() { + val path = incrementalPathOf(infra.rawInfra, infra.blockInfra) + val automaton = + SpacingRequirementAutomaton( + infra.rawInfra, + infra.loadedSignalInfra, + infra.blockInfra, + infra.signalingSimulator, + makeCallbacks(blockLengths[0].distance, false), + path + ) + val blocks = + mutableStaticIdxArrayListOf( + blocks[0], + blocks[1], + blocks[2], + ) + path.extend( + PathFragment( + mutableStaticIdxArrayListOf(routes[0]), + blocks, + stops = listOf(), + containsStart = true, + containsEnd = false, + 0.meters, + 0.meters + ) + ) + val iterationResult = automaton.processPathUpdate() + + // We should have just enough data to generate resource use + assertTrue { iterationResult != NotEnoughPath } + } } /** Returns an incremental requirement callback of the given length */