diff --git a/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/SimulationScheduleItemsParser.kt b/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/SimulationScheduleItemsParser.kt new file mode 100644 index 00000000000..b674e7885ef --- /dev/null +++ b/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/SimulationScheduleItemsParser.kt @@ -0,0 +1,53 @@ +package fr.sncf.osrd.api.api_v2 + +import fr.sncf.osrd.api.api_v2.standalone_sim.SimulationScheduleItem +import fr.sncf.osrd.utils.units.TimeDelta +import java.lang.Long.min + +/** + * Merge consecutive schedule items which are at the same location, i.e. have the same path offset. + * Merge rules for a group of schedule items are as follows: + * - arrival is the minimum value of all the concerned schedule items, null if they are all null. + * - stopFor is the sum of all the stopFor values of all the schedule items, null if they are all + * null. + * - onStopSignal is true if at least one schedule item's onStopSignal is true, false otherwise. + */ +fun parseRawSimulationScheduleItems( + rawSimulationScheduleItems: List +): List { + val simulationScheduleItems = mutableListOf() + var i = 0 + while (i < rawSimulationScheduleItems.size) { + val pathOffset = rawSimulationScheduleItems[i].pathOffset + var arrival = rawSimulationScheduleItems[i].arrival + var stopFor = rawSimulationScheduleItems[i].stopFor + var onStopSignal = rawSimulationScheduleItems[i].onStopSignal + while ( + i < rawSimulationScheduleItems.size - 1 && + rawSimulationScheduleItems[i + 1].pathOffset == pathOffset + ) { + val nextArrival = rawSimulationScheduleItems[i + 1].arrival + arrival = + when { + nextArrival != null && arrival != null -> + TimeDelta(min(arrival.milliseconds, nextArrival.milliseconds)) + nextArrival != null -> nextArrival + else -> arrival + } + val nextStopFor = rawSimulationScheduleItems[i + 1].stopFor + stopFor = + when { + nextStopFor != null && stopFor != null -> stopFor + nextStopFor + nextStopFor != null -> nextStopFor + else -> stopFor + } + onStopSignal = onStopSignal || rawSimulationScheduleItems[i + 1].onStopSignal + i++ + } + simulationScheduleItems.add( + SimulationScheduleItem(pathOffset, arrival, stopFor, onStopSignal) + ) + i++ + } + return simulationScheduleItems +} diff --git a/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/standalone_sim/SimulationEndpoint.kt b/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/standalone_sim/SimulationEndpoint.kt index 20ffab0eed6..445a308aeb9 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/standalone_sim/SimulationEndpoint.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/standalone_sim/SimulationEndpoint.kt @@ -3,6 +3,7 @@ package fr.sncf.osrd.api.api_v2.standalone_sim import fr.sncf.osrd.api.ElectricalProfileSetManager import fr.sncf.osrd.api.ExceptionHandler import fr.sncf.osrd.api.InfraManager +import fr.sncf.osrd.api.api_v2.parseRawSimulationScheduleItems import fr.sncf.osrd.api.pathfinding.makeChunkPath import fr.sncf.osrd.reporting.exceptions.OSRDError import fr.sncf.osrd.reporting.warnings.DiagnosticRecorderImpl @@ -65,7 +66,7 @@ class SimulationEndpoint( parsePowerRestrictions(request.powerRestrictions), request.options.useElectricalProfiles, 2.0, - request.schedule, + parseRawSimulationScheduleItems(request.schedule), request.initialSpeed, request.margins, ) diff --git a/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/stdcm/STDCMEndpointV2.kt b/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/stdcm/STDCMEndpointV2.kt index beae6ed2dab..2986d06dc73 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/stdcm/STDCMEndpointV2.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/api/api_v2/stdcm/STDCMEndpointV2.kt @@ -6,10 +6,7 @@ import fr.sncf.osrd.api.InfraManager import fr.sncf.osrd.api.api_v2.* import fr.sncf.osrd.api.api_v2.pathfinding.findWaypointBlocks import fr.sncf.osrd.api.api_v2.pathfinding.runPathfindingPostProcessing -import fr.sncf.osrd.api.api_v2.standalone_sim.MarginValue -import fr.sncf.osrd.api.api_v2.standalone_sim.SimulationScheduleItem -import fr.sncf.osrd.api.api_v2.standalone_sim.SimulationSuccess -import fr.sncf.osrd.api.api_v2.standalone_sim.parseRawRollingStock +import fr.sncf.osrd.api.api_v2.standalone_sim.* import fr.sncf.osrd.conflicts.* import fr.sncf.osrd.envelope_sim.allowances.utils.AllowanceValue import fr.sncf.osrd.envelope_sim.allowances.utils.AllowanceValue.Percentage @@ -155,9 +152,11 @@ private fun parseMarginValue(margin: MarginValue): AllowanceValue? { private fun parseSimulationScheduleItems( trainStops: List ): List { - return trainStops.map { - SimulationScheduleItem(Offset(it.position.meters), null, it.duration.seconds, true) - } + return parseRawSimulationScheduleItems( + trainStops.map { + SimulationScheduleItem(Offset(it.position.meters), null, it.duration.seconds, true) + } + ) } /** Sanity check, we assert that the result is not conflicting with the scheduled timetable */ diff --git a/core/src/test/kotlin/fr/sncf/osrd/standalone_sim/SimulationScheduleItemsParserTests.kt b/core/src/test/kotlin/fr/sncf/osrd/standalone_sim/SimulationScheduleItemsParserTests.kt new file mode 100644 index 00000000000..5f02d04bded --- /dev/null +++ b/core/src/test/kotlin/fr/sncf/osrd/standalone_sim/SimulationScheduleItemsParserTests.kt @@ -0,0 +1,75 @@ +package fr.sncf.osrd.standalone_sim + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import fr.sncf.osrd.api.api_v2.parseRawSimulationScheduleItems +import fr.sncf.osrd.api.api_v2.standalone_sim.SimulationScheduleItem +import fr.sncf.osrd.utils.units.Distance +import fr.sncf.osrd.utils.units.Offset +import fr.sncf.osrd.utils.units.TimeDelta +import java.util.stream.Stream +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SimulationScheduleItemsParserTests { + + @ParameterizedTest + @MethodSource("testParseRawSimulationScheduleItemsArgs") + fun parserOutputsMinimumArrivalForSamePathOffset( + simulationScheduleItems: List, + expectedItems: List + ) { + val mergedItems = parseRawSimulationScheduleItems(simulationScheduleItems) + Assertions.assertThat(mergedItems).usingRecursiveComparison().isEqualTo(expectedItems) + } + + @SuppressFBWarnings( + value = ["UPM_UNCALLED_PRIVATE_METHOD"], + justification = "called implicitly by MethodSource" + ) + private fun testParseRawSimulationScheduleItemsArgs(): Stream { + return Stream.of( + // Parser outputs minimum arrival for same path offset + Arguments.of( + listOf( + SimulationScheduleItem(Offset(Distance.ZERO), null, null, false), + SimulationScheduleItem(Offset(Distance(1000)), TimeDelta(200), null, false), + SimulationScheduleItem(Offset(Distance(1000)), TimeDelta(100), null, false), + SimulationScheduleItem(Offset(Distance(1000)), null, null, false), + ), + listOf( + SimulationScheduleItem(Offset(Distance.ZERO), null, null, false), + SimulationScheduleItem(Offset(Distance(1000)), TimeDelta(100), null, false), + ) + ), + // Parser outputs sum of stopFor for same path offset + Arguments.of( + listOf( + SimulationScheduleItem(Offset(Distance.ZERO), null, null, false), + SimulationScheduleItem(Offset(Distance(1000)), null, TimeDelta(25), false), + SimulationScheduleItem(Offset(Distance(1000)), null, TimeDelta(75), false), + SimulationScheduleItem(Offset(Distance(1000)), null, null, false), + ), + listOf( + SimulationScheduleItem(Offset(Distance.ZERO), null, null, false), + SimulationScheduleItem(Offset(Distance(1000)), null, TimeDelta(100), false), + ) + ), + // Parser outputs true if at least one onStopSignal is true for same path offset + Arguments.of( + listOf( + SimulationScheduleItem(Offset(Distance.ZERO), null, null, false), + SimulationScheduleItem(Offset(Distance(1000)), null, null, false), + SimulationScheduleItem(Offset(Distance(1000)), null, null, true), + ), + listOf( + SimulationScheduleItem(Offset(Distance.ZERO), null, null, false), + SimulationScheduleItem(Offset(Distance(1000)), null, null, true), + ) + ) + ) + } +}