diff --git a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/EngineeringAllowanceManager.kt b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/EngineeringAllowanceManager.kt index dc68eea38dc..a629b1dbb4f 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/EngineeringAllowanceManager.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/EngineeringAllowanceManager.kt @@ -19,6 +19,7 @@ import fr.sncf.osrd.envelope_sim_infra.computeMRSP import fr.sncf.osrd.graph.PathfindingEdgeRangeId import fr.sncf.osrd.reporting.exceptions.OSRDError import fr.sncf.osrd.utils.SelfTypeHolder +import fr.sncf.osrd.utils.units.Distance import fr.sncf.osrd.utils.units.meters import fr.sncf.osrd.utils.units.sumDistances import java.util.* @@ -31,26 +32,27 @@ class EngineeringAllowanceManager(private val graph: STDCMGraph) { /** * Check whether an engineering allowance can be used in this context to be at the expected - * start time at the node location. + * start time at the node location. Returns the allowance length if it's possible, or null if it + * isn't. */ - fun checkEngineeringAllowance(prevNode: STDCMNode, expectedStartTime: Double): Boolean { + fun checkEngineeringAllowance(prevNode: STDCMNode, expectedStartTime: Double): Distance? { if (prevNode.previousEdge == null) - return false // The conflict happens on the first block, we can't add delay here + return null // The conflict happens on the first block, we can't add delay here val affectedEdges = findAffectedEdges( prevNode.previousEdge, expectedStartTime - prevNode.timeData.earliestReachableTime ) - if (affectedEdges.isEmpty()) return false // No space to try the allowance + if (affectedEdges.isEmpty()) return null // No space to try the allowance val length = affectedEdges.map { it.length.distance }.sumDistances() if (length > 50_000.meters) { // If the allowance area is large enough to reasonably stop and accelerate again, we // just accept the solution. This avoids computation on very large paths // (which can be quite time expensive) - return true + return length } - if (length == 0.meters) return false + if (length == 0.meters) return null // We try to run a simulation with the slowest running time while keeping the end time // identical. @@ -65,7 +67,7 @@ class EngineeringAllowanceManager(private val graph: STDCMGraph) { firstNode.timeData.earliestReachableTime + firstNode.timeData.maxDepartureDelayingWithoutConflict + slowestRunningTime - return latestArrivalTime >= expectedStartTime + return if (latestArrivalTime >= expectedStartTime) length else null } /** diff --git a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/PostProcessingSimulation.kt b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/PostProcessingSimulation.kt index 181dcdeaa40..fc7648b7b0c 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/PostProcessingSimulation.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/PostProcessingSimulation.kt @@ -196,20 +196,30 @@ private fun initFixedPoints( res.add(makeFixedPoint(res, edges, length, length, updatedTimeData, 0.0)) // Add points at the end of each engineering allowance + fun addFixedPointAvoidingDuplicates(offset: Distance) { + if (res.none { it.offset.distance == offset }) { + res.add( + makeFixedPoint( + res, + edges, + Offset(offset), + length, + updatedTimeData, + ) + ) + } + } var prevEdgeLength = 0.meters for (edge in edges) { - if (edge.afterEngineeringAllowance) { - if (res.none { it.offset.distance == prevEdgeLength }) { - res.add( - makeFixedPoint( - res, - edges, - Offset(prevEdgeLength), - length, - updatedTimeData, - ) - ) - } + val engineeringAllowanceLength = edge.engineeringAllowanceLength + if (engineeringAllowanceLength != null) { + val engineeringAllowanceStart = prevEdgeLength - engineeringAllowanceLength + // Edges can have overlapping engineering allowance, only the last one is relevant. + // So we remove any point in the current allowance range. + res.removeIf { it.offset.distance > engineeringAllowanceStart && it.stopTime != null } + + addFixedPointAvoidingDuplicates(prevEdgeLength) + addFixedPointAvoidingDuplicates(engineeringAllowanceStart) } prevEdgeLength += edge.length.distance } diff --git a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMEdge.kt b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMEdge.kt index e2c43bd1200..d5c4ca85d93 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMEdge.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMEdge.kt @@ -4,6 +4,7 @@ import fr.sncf.osrd.sim_infra.api.Block import fr.sncf.osrd.sim_infra.api.Path import fr.sncf.osrd.sim_infra.api.TravelledPath import fr.sncf.osrd.stdcm.infra_exploration.InfraExplorerWithEnvelope +import fr.sncf.osrd.utils.units.Distance import fr.sncf.osrd.utils.units.Length import fr.sncf.osrd.utils.units.Offset import fr.sncf.osrd.utils.units.meters @@ -34,9 +35,9 @@ data class STDCMEdge( // How long it takes to go from the beginning to the end of the block, taking the // standard allowance into account val totalTime: Double, - // Set to true if a conflict in the current edge required an engineering allowance. - // Used for initial placement of fixed time points in post-processing. - val afterEngineeringAllowance: Boolean, + // If this edges starts after the end of an engineering allowance, this contains its length, or + // null otherwise. Used for initial placement of fixed time points in postprocessing. + val engineeringAllowanceLength: Distance?, ) { val block = infraExplorer.getCurrentBlock() diff --git a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMEdgeBuilder.kt b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMEdgeBuilder.kt index 7ee219a5bd7..8ff5044d076 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMEdgeBuilder.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/stdcm/graph/STDCMEdgeBuilder.kt @@ -5,6 +5,7 @@ import fr.sncf.osrd.envelope_sim.allowances.LinearAllowance import fr.sncf.osrd.sim_infra.api.Block import fr.sncf.osrd.stdcm.infra_exploration.InfraExplorerWithEnvelope import fr.sncf.osrd.stdcm.preprocessing.interfaces.BlockAvailabilityInterface +import fr.sncf.osrd.utils.units.Distance import fr.sncf.osrd.utils.units.Distance.Companion.fromMeters import fr.sncf.osrd.utils.units.Length import fr.sncf.osrd.utils.units.Offset @@ -160,11 +161,13 @@ internal constructor( var departureTimeShift = delayNeeded val needEngineeringAllowance = delayNeeded > prevNode.timeData.maxDepartureDelayingWithoutConflict + var allowanceLength: Distance? = null if (needEngineeringAllowance) { // We can't just shift the departure time, we need an engineering allowance // It's not computed yet, we just check that it's possible - if (!graph.allowanceManager.checkEngineeringAllowance(prevNode, actualStartTime)) - return null + allowanceLength = + graph.allowanceManager.checkEngineeringAllowance(prevNode, actualStartTime) + ?: return null // We still need to adapt the delay values departureTimeShift = prevNode.timeData.maxDepartureDelayingWithoutConflict } else { @@ -214,7 +217,7 @@ internal constructor( envelope!!.endSpeed, Length(fromMeters(envelope!!.endPos)), envelope!!.totalTime / standardAllowanceSpeedRatio, - needEngineeringAllowance, + allowanceLength, ) res = graph.backtrackingManager.backtrack(res!!, envelope!!) return if (res == null || graph.delayManager.isRunTimeTooLong(res)) null else res diff --git a/core/src/test/kotlin/fr/sncf/osrd/stdcm/preprocessing/STDCMHeuristicTests.kt b/core/src/test/kotlin/fr/sncf/osrd/stdcm/preprocessing/STDCMHeuristicTests.kt index ad849a308c9..b8e0137e27a 100644 --- a/core/src/test/kotlin/fr/sncf/osrd/stdcm/preprocessing/STDCMHeuristicTests.kt +++ b/core/src/test/kotlin/fr/sncf/osrd/stdcm/preprocessing/STDCMHeuristicTests.kt @@ -253,7 +253,7 @@ class STDCMHeuristicTests { 0.0, Length(0.meters), 0.0, - false, + null, ) return heuristic.invoke(defaultEdge, nodeOffsetOnEdge?.let { Offset(it) }, nbPassedSteps) }