From 707d6faa53a8ba52b24ee3b12b7f893f3c16fde2 Mon Sep 17 00:00:00 2001 From: Eloi Charpentier Date: Fri, 29 Sep 2023 16:24:26 +0200 Subject: [PATCH 1/2] core: stdcm: use spacing requirements instead of route occupancies --- .../osrd/sim_infra/api/InterlockingInfra.kt | 10 +- .../sim_infra/api/LoadedSignalingInfra.kt | 8 +- .../osrd/sim_infra/impl/BlockInfraImpl.kt | 24 ++- .../osrd/sim_infra/impl/RawInfraBuilder.kt | 2 +- .../sncf/osrd/sim_infra/impl/RawInfraImpl.kt | 26 ++- .../osrd/sim_infra/utils/BlockRecovery.kt | 2 +- .../sncf/osrd/signaling/bal/TestBALtoBAL.kt | 4 +- .../sncf/osrd/signaling/bal/TestBAPRtoBAL.kt | 6 +- core/openapi.yaml | 14 +- .../fr/sncf/osrd/api/stdcm/STDCMEndpoint.java | 30 +--- .../fr/sncf/osrd/api/stdcm/STDCMRequest.java | 37 +--- .../ScheduleMetadataExtractor.kt | 31 ---- .../standalone_sim/result/ResultTrain.java | 7 +- .../fr/sncf/osrd/stdcm/graph/STDCMGraph.java | 2 +- .../UnavailableSpaceBuilder.java | 163 ++++++++---------- .../sncf/osrd/utils/graph/GraphAdapter.java | 2 +- .../osrd/api/StandaloneSimulationTest.java | 1 - .../fr/sncf/osrd/stdcm/FullSTDCMTests.java | 74 +++++++- .../java/fr/sncf/osrd/stdcm/STDCMHelpers.java | 34 ++-- .../BlockAvailabilityLegacyAdapterTests.java | 13 +- .../UnavailableSpaceBuilderTests.java | 49 +++--- .../kotlin/fr/sncf/osrd/utils/DummyInfra.kt | 21 ++- 22 files changed, 285 insertions(+), 275 deletions(-) diff --git a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/api/InterlockingInfra.kt b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/api/InterlockingInfra.kt index b9bb4d66337..f85288a8e9e 100644 --- a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/api/InterlockingInfra.kt +++ b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/api/InterlockingInfra.kt @@ -18,10 +18,15 @@ import fr.sncf.osrd.utils.units.OffsetList sealed interface Zone typealias ZoneId = StaticIdx +@Suppress("INAPPLICABLE_JVM_NAME") interface LocationInfra : TrackNetworkInfra, TrackInfra, TrackProperties { val zones: StaticIdxSpace fun getMovableElements(zone: ZoneId): StaticIdxSortedSet fun getZoneBounds(zone: ZoneId): List + @JvmName("getZoneName") + fun getZoneName(zone: ZoneId): String + @JvmName("getZoneFromName") + fun getZoneFromName(name: String): ZoneId val detectors: StaticIdxSpace fun getNextZone(dirDet: DirDetectorId): ZoneId? @@ -29,10 +34,6 @@ interface LocationInfra : TrackNetworkInfra, TrackInfra, TrackProperties { fun getDetectorName(det: DetectorId): String? } -fun LocationInfra.getZoneName(zone: ZoneId): String { - return "zone.${getZoneBounds(zone).map { "${getDetectorName(it.value)}:${it.direction}" }.minOf { it }}" -} - fun LocationInfra.isBufferStop(detector: StaticIdx): Boolean { return getNextZone(detector.increasing) == null || getNextZone(detector.decreasing) == null } @@ -60,6 +61,7 @@ interface ReservationInfra : LocationInfra { fun getZonePathChunks(zonePath: ZonePathId): DirStaticIdxList } +@JvmName("getZonePathZone") fun ReservationInfra.getZonePathZone(zonePath: ZonePathId): ZoneId { return getNextZone(getZonePathEntry(zonePath))!! } diff --git a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/api/LoadedSignalingInfra.kt b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/api/LoadedSignalingInfra.kt index 6be1e2385e3..527fcfc7a6e 100644 --- a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/api/LoadedSignalingInfra.kt +++ b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/api/LoadedSignalingInfra.kt @@ -58,13 +58,17 @@ interface BlockInfra { val blocks: StaticIdxSpace @JvmName("getBlockPath") fun getBlockPath(block: BlockId): StaticIdxList + @JvmName("getBlocksInZone") + fun getBlocksInZone(zone: ZoneId): StaticIdxList fun getBlockSignals(block: BlockId): StaticIdxList fun blockStartAtBufferStop(block: BlockId): Boolean fun blockStopAtBufferStop(block: BlockId): Boolean fun getBlockSignalingSystem(block: BlockId): SignalingSystemId - @JvmName("getBlocksAtDetector") - fun getBlocksAtDetector(detector: DirDetectorId): StaticIdxList + @JvmName("getBlocksStartingAtDetector") + fun getBlocksStartingAtDetector(detector: DirDetectorId): StaticIdxList + @JvmName("getBlocksEndingAtDetector") + fun getBlocksEndingAtDetector(detector: DirDetectorId): StaticIdxList fun getBlocksAtSignal(signal: LogicalSignalId): StaticIdxList fun getSignalsPositions(block: BlockId): OffsetList @JvmName("getBlocksFromTrackChunk") diff --git a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/BlockInfraImpl.kt b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/BlockInfraImpl.kt index d86f30bd810..792ccaf109c 100644 --- a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/BlockInfraImpl.kt +++ b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/BlockInfraImpl.kt @@ -41,19 +41,25 @@ class BlockInfraImpl( private val rawInfra: RawInfra, ) : BlockInfra { private val blockEntryDetectorMap = IdxMap>() + private val blockExitDetectorMap = IdxMap>() private val blockEntrySignalMap = IdxMap>() private val trackChunkToBlockMap = IdxMap, MutableStaticIdxArraySet>() private val blockToTrackChunkMap = IdxMap, MutableDirStaticIdxList>() + private val zoneToBlockMap = IdxMap>() init { for (blockId in blockPool.space()) { val block = blockPool[blockId] val entryZonePath = block.path[0] + val exitZonePath = block.path[block.path.size - 1] // Update blockEntryDetectorMap val entryDirDet = rawInfra.getZonePathEntry(entryZonePath) - val detList = blockEntryDetectorMap.getOrPut(entryDirDet) { mutableStaticIdxArrayListOf() } - detList.add(blockId) + val exitDirDet = rawInfra.getZonePathExit(exitZonePath) + val entryDetList = blockEntryDetectorMap.getOrPut(entryDirDet) { mutableStaticIdxArrayListOf() } + val exitDetList = blockExitDetectorMap.getOrPut(exitDirDet) { mutableStaticIdxArrayListOf() } + entryDetList.add(blockId) + exitDetList.add(blockId) // Update blockEntrySignalMap if (!block.startAtBufferStop) { @@ -62,7 +68,7 @@ class BlockInfraImpl( sigList.add(blockId) } - // Update trackChunkToBlockMap and blockToTrackChunkMap + // Update trackChunkToBlockMap, blockToTrackChunkMap, and zoneToBlockMap for (zonePath in getBlockPath(blockId)) { val trackChunks = rawInfra.getZonePathChunks(zonePath) val blockTrackChunks = blockToTrackChunkMap.getOrPut(blockId) { mutableDirStaticIdxArrayListOf() } @@ -71,6 +77,8 @@ class BlockInfraImpl( val chunkBlocks = trackChunkToBlockMap.getOrPut(trackChunk) { mutableStaticIdxArraySetOf() } chunkBlocks.add(blockId) } + zoneToBlockMap.getOrPut(rawInfra.getZonePathZone(zonePath)) { mutableStaticIdxArrayListOf() } + .add(blockId) } } } @@ -82,6 +90,10 @@ class BlockInfraImpl( return blockPool[block].path } + override fun getBlocksInZone(zone: ZoneId): StaticIdxList { + return zoneToBlockMap[zone]!! + } + override fun getBlockSignals(block: BlockId): StaticIdxList { return blockPool[block].signals } @@ -98,10 +110,14 @@ class BlockInfraImpl( return loadedSignalInfra.getSignalingSystem(blockPool[block].signals[0]) } - override fun getBlocksAtDetector(detector: DirDetectorId): StaticIdxList { + override fun getBlocksStartingAtDetector(detector: DirDetectorId): StaticIdxList { return blockEntryDetectorMap[detector] ?: mutableStaticIdxArrayListOf() } + override fun getBlocksEndingAtDetector(detector: DirDetectorId): StaticIdxList { + return blockExitDetectorMap[detector] ?: mutableStaticIdxArrayListOf() + } + override fun getBlocksAtSignal(signal: LogicalSignalId): StaticIdxList { return blockEntrySignalMap[signal] ?: mutableStaticIdxArrayListOf() } diff --git a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/RawInfraBuilder.kt b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/RawInfraBuilder.kt index ac7213e5c53..cd5212accdf 100644 --- a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/RawInfraBuilder.kt +++ b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/RawInfraBuilder.kt @@ -447,7 +447,7 @@ class RawInfraBuilderImpl : RawInfraBuilder { makeTrackNameMap(), makeRouteNameMap(), makeDetEntryToRouteMap(), - makeDetExitToRouteMap() + makeDetExitToRouteMap(), ) } diff --git a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/RawInfraImpl.kt b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/RawInfraImpl.kt index fa8c7685fcf..6abb5e80803 100644 --- a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/RawInfraImpl.kt +++ b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/impl/RawInfraImpl.kt @@ -91,8 +91,10 @@ class TrackChunkDescriptor( val speedSections: DirectionalMap> ) -@JvmInline -value class ZoneDescriptor(val movableElements: StaticIdxSortedSet) +class ZoneDescriptor( + val movableElements: StaticIdxSortedSet, + var name: String = "", +) interface RouteDescriptor { val name: String? @@ -186,7 +188,8 @@ class RawInfraImpl( val trackSectionNameMap: Map, val routeNameMap: Map, val dirDetEntryToRouteMap: Map>, - val dirDetExitToRouteMap: Map> + val dirDetExitToRouteMap: Map>, + val zoneNameMap: HashMap = HashMap(), ) : RawInfra { override val trackNodes: StaticIdxSpace get() = trackNodePool.space() @@ -307,6 +310,15 @@ class RawInfraImpl( zoneDetectors[prevZone]!!.add(detector.decreasing) } + // initialize zone names + for (zone in zonePool) { + val name = getZoneBounds(zone) + .sortedBy { id -> id.index } + .map { "${getDetectorName(it.value)}:${it.direction}" } + zonePool[zone].name = "zone.${name}" + zoneNameMap[zonePool[zone].name] = zone + } + // initialize the physical signal to logical signal map for (physicalSignal in physicalSignalPool) for (child in physicalSignalPool[physicalSignal].logicalSignals) @@ -360,6 +372,14 @@ class RawInfraImpl( return zoneDetectors[zone]!! } + override fun getZoneName(zone: ZoneId): String { + return zonePool[zone].name + } + + override fun getZoneFromName(name: String): ZoneId { + return zoneNameMap[name]!! + } + override val detectors: StaticIdxSpace get() = detectorPool.space() diff --git a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/utils/BlockRecovery.kt b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/utils/BlockRecovery.kt index 6d305eb5da8..a60c41e6c3e 100644 --- a/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/utils/BlockRecovery.kt +++ b/core/kt-osrd-sim-infra/src/main/kotlin/fr/sncf/osrd/sim_infra/utils/BlockRecovery.kt @@ -111,7 +111,7 @@ private fun findRouteBlocks( // initialize with the BlockPathElements which are acceptable at the start of the route if (previousPaths == null) { val currentDet = signalingInfra.getZonePathEntry(routePath[0]) - val blocks = blockInfra.getBlocksAtDetector(currentDet) + val blocks = blockInfra.getBlocksStartingAtDetector(currentDet) val blocksOnRoute = filterBlocks(allowedSignalingSystems, blockInfra, blocks, routePath, 0) for (block in blocksOnRoute) { val blockPath = blockInfra.getBlockPath(block) diff --git a/core/kt-osrd-sncf-signaling/src/test/kotlin/fr/sncf/osrd/signaling/bal/TestBALtoBAL.kt b/core/kt-osrd-sncf-signaling/src/test/kotlin/fr/sncf/osrd/signaling/bal/TestBALtoBAL.kt index 608ac29d287..8db0fb4e268 100644 --- a/core/kt-osrd-sncf-signaling/src/test/kotlin/fr/sncf/osrd/signaling/bal/TestBALtoBAL.kt +++ b/core/kt-osrd-sncf-signaling/src/test/kotlin/fr/sncf/osrd/signaling/bal/TestBALtoBAL.kt @@ -117,8 +117,8 @@ class TestBALtoBAL { val loadedSignalInfra = simulator.loadSignals(infra) val blockInfra = simulator.buildBlocks(infra, loadedSignalInfra) val fullPath = mutableStaticIdxArrayListOf() - fullPath.add(blockInfra.getBlocksAtDetector(detectorU.increasing).first()) - fullPath.add(blockInfra.getBlocksAtDetector(detectorV.increasing).first()) + fullPath.add(blockInfra.getBlocksStartingAtDetector(detectorU.increasing).first()) + fullPath.add(blockInfra.getBlocksStartingAtDetector(detectorV.increasing).first()) val zoneStates = mutableListOf(ZoneStatus.CLEAR, ZoneStatus.CLEAR, ZoneStatus.CLEAR) val res = simulator.evaluate(infra, loadedSignalInfra, blockInfra, fullPath, 0, fullPath.size, zoneStates, ZoneStatus.INCOMPATIBLE) assertEquals("A", res[loadedSignalInfra.getLogicalSignals(signalV).first()]!!.getEnum("aspect")) diff --git a/core/kt-osrd-sncf-signaling/src/test/kotlin/fr/sncf/osrd/signaling/bal/TestBAPRtoBAL.kt b/core/kt-osrd-sncf-signaling/src/test/kotlin/fr/sncf/osrd/signaling/bal/TestBAPRtoBAL.kt index 339c8adaaff..7e7f3ea227d 100644 --- a/core/kt-osrd-sncf-signaling/src/test/kotlin/fr/sncf/osrd/signaling/bal/TestBAPRtoBAL.kt +++ b/core/kt-osrd-sncf-signaling/src/test/kotlin/fr/sncf/osrd/signaling/bal/TestBAPRtoBAL.kt @@ -98,9 +98,9 @@ class TestBAPRtoBAL { val loadedSignalInfra = simulator.loadSignals(infra) val blockInfra = simulator.buildBlocks(infra, loadedSignalInfra) val fullPath = mutableStaticIdxArrayListOf() - fullPath.add(blockInfra.getBlocksAtDetector(detectorW.increasing).first()) - fullPath.add(blockInfra.getBlocksAtDetector(detectorX.increasing).first()) - fullPath.add(blockInfra.getBlocksAtDetector(detectorY.increasing).first()) + fullPath.add(blockInfra.getBlocksStartingAtDetector(detectorW.increasing).first()) + fullPath.add(blockInfra.getBlocksStartingAtDetector(detectorX.increasing).first()) + fullPath.add(blockInfra.getBlocksStartingAtDetector(detectorY.increasing).first()) val zoneStates = mutableListOf(ZoneStatus.CLEAR, ZoneStatus.CLEAR, ZoneStatus.INCOMPATIBLE) val res = simulator.evaluate(infra, loadedSignalInfra, blockInfra, fullPath, 0, fullPath.size, zoneStates, ZoneStatus.INCOMPATIBLE) val logicalSignals = listOf(signalm, signalM, signaln, signalN).map{loadedSignalInfra.getLogicalSignals(it).first()} diff --git a/core/openapi.yaml b/core/openapi.yaml index 29f91c65ad6..c044f03dbc8 100644 --- a/core/openapi.yaml +++ b/core/openapi.yaml @@ -754,20 +754,10 @@ components: example: "infraID" rolling_stocks: $ref: "#/components/schemas/RollingStock" - route_occupancies: + spacing_requirements: type: array items: - type: object - properties: - id: - type: string - description: Route ID - start_occupancy_time: - type: number - format: double - end_occupancy_time: - type: number - format: double + $ref: "#/components/schemas/SpacingRequirement" start_time: type: number format: double diff --git a/core/src/main/java/fr/sncf/osrd/api/stdcm/STDCMEndpoint.java b/core/src/main/java/fr/sncf/osrd/api/stdcm/STDCMEndpoint.java index 5cd11b066a3..75fddddd467 100644 --- a/core/src/main/java/fr/sncf/osrd/api/stdcm/STDCMEndpoint.java +++ b/core/src/main/java/fr/sncf/osrd/api/stdcm/STDCMEndpoint.java @@ -1,7 +1,5 @@ package fr.sncf.osrd.api.stdcm; -import static fr.sncf.osrd.utils.KtToJavaConverter.toIntList; - import fr.sncf.osrd.api.ExceptionHandler; import fr.sncf.osrd.api.FullInfra; import fr.sncf.osrd.api.InfraManager; @@ -14,8 +12,6 @@ import fr.sncf.osrd.reporting.exceptions.ErrorType; import fr.sncf.osrd.reporting.exceptions.OSRDError; import fr.sncf.osrd.reporting.warnings.DiagnosticRecorderImpl; -import fr.sncf.osrd.sim_infra.api.InterlockingInfraKt; -import fr.sncf.osrd.sim_infra.api.RawSignalingInfra; import fr.sncf.osrd.standalone_sim.ScheduleMetadataExtractor; import fr.sncf.osrd.standalone_sim.result.ResultEnvelopePoint; import fr.sncf.osrd.standalone_sim.result.StandaloneSimResult; @@ -35,8 +31,6 @@ import org.takes.rs.RsWithBody; import org.takes.rs.RsWithStatus; import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; import java.util.List; public class STDCMEndpoint implements Take { @@ -70,7 +64,6 @@ public Response act(Request req) throws OSRDError { final var comfort = RJSRollingStockParser.parseComfort(request.comfort); final var steps = parseSteps(infra, request.steps); final String tag = request.speedLimitComposition; - var occupancies = request.routeOccupancies; AllowanceValue standardAllowance = null; if (request.standardAllowance != null) standardAllowance = RJSStandaloneTrainScheduleParser.parseAllowanceValue( @@ -81,11 +74,10 @@ public Response act(Request req) throws OSRDError { // Build the unavailable space // temporary workaround, to remove with new signaling - occupancies = addWarningOccupancies(infra.rawInfra(), occupancies); var unavailableSpace = UnavailableSpaceBuilder.computeUnavailableSpace( infra.rawInfra(), infra.blockInfra(), - occupancies, + request.spacingRequirements, rollingStock, request.gridMarginAfterSTDCM, request.gridMarginBeforeSTDCM @@ -140,26 +132,8 @@ private static List parseSteps(FullInfra infra, List addWarningOccupancies( - RawSignalingInfra rawInfra, - Collection occupancies - ) { - var warningOccupancies = new HashSet<>(occupancies); - for (var occupancy : occupancies) { - var route = rawInfra.getRouteFromName(occupancy.id); - var previousRoutes = toIntList(rawInfra.getRoutesEndingAtDet( - InterlockingInfraKt.getRouteEntry(rawInfra, route))); - for (var previousRoute : previousRoutes) - warningOccupancies.add(new STDCMRequest.RouteOccupancy(rawInfra.getRouteName(previousRoute), - occupancy.startOccupancyTime, occupancy.endOccupancyTime)); - } - return warningOccupancies; - } - /** Generate a train schedule matching the envelope and rolling stock, with one stop at the end */ - private static StandaloneTrainSchedule makeTrainSchedule( + public static StandaloneTrainSchedule makeTrainSchedule( double endPos, RollingStock rollingStock, RollingStock.Comfort comfort, diff --git a/core/src/main/java/fr/sncf/osrd/api/stdcm/STDCMRequest.java b/core/src/main/java/fr/sncf/osrd/api/stdcm/STDCMRequest.java index a97db33f91e..70c42fbbf34 100644 --- a/core/src/main/java/fr/sncf/osrd/api/stdcm/STDCMRequest.java +++ b/core/src/main/java/fr/sncf/osrd/api/stdcm/STDCMRequest.java @@ -11,6 +11,7 @@ import fr.sncf.osrd.railjson.schema.rollingstock.RJSRollingStock; import fr.sncf.osrd.railjson.schema.schedule.RJSAllowance; import fr.sncf.osrd.railjson.schema.schedule.RJSAllowanceValue; +import fr.sncf.osrd.standalone_sim.result.ResultTrain; import java.util.Collection; import java.util.List; @@ -46,10 +47,10 @@ public final class STDCMRequest { public RJSComfortType comfort; /** - * Route occupancies in the given timetable + * Spacing requirements for any train in the timetable */ - @Json(name = "route_occupancies") - public Collection routeOccupancies; + @Json(name = "spacing_requirements") + public Collection spacingRequirements; /** A list of steps on the path. A step is a set of location with a stop duration. * The path only has to go through a single point per location. @@ -142,7 +143,7 @@ public STDCMRequest( String infra, String expectedVersion, RJSRollingStock rollingStock, - Collection routeOccupancies, + Collection spacingRequirements, List steps, double startTime, double endTime, @@ -154,7 +155,7 @@ public STDCMRequest( this.infra = infra; this.expectedVersion = expectedVersion; this.rollingStock = rollingStock; - this.routeOccupancies = routeOccupancies; + this.spacingRequirements = spacingRequirements; this.steps = steps; this.startTime = startTime; this.endTime = endTime; @@ -184,30 +185,4 @@ public STDCMStep(double stopDuration, boolean stop, Collection, - simInfraAdapter: SimInfraAdapter, - envelope: EnvelopeTimeInterpolate -): Map { - val routeOccupancies = mutableMapOf() - val zoneOccupationChangeEventsByRoute = mutableMapOf>() - for (event in zoneOccupationChangeEvents) { - for (route in simInfraAdapter.zoneMap.inverse()[event.zone]!!.routes) { - zoneOccupationChangeEventsByRoute.getOrPut(route.id) { mutableListOf() }.add(event) - } - } - - for ((route, events) in zoneOccupationChangeEventsByRoute) { - events.sortBy { it.time } - val entry = events.first() - assert(entry.isEntry) - val exit = events.last() - if (exit.isEntry) { - routeOccupancies[route] = ResultOccupancyTiming(entry.time.toDouble() / 1000, envelope.totalTime) - continue - } - routeOccupancies[route] = ResultOccupancyTiming(entry.time.toDouble() / 1000, exit.time.toDouble() / 1000) - } - return routeOccupancies -} - private data class ZoneOccupationChangeEvent( val time: Long, val offset: Distance, diff --git a/core/src/main/java/fr/sncf/osrd/standalone_sim/result/ResultTrain.java b/core/src/main/java/fr/sncf/osrd/standalone_sim/result/ResultTrain.java index fe65a2f760d..41fd3aba859 100644 --- a/core/src/main/java/fr/sncf/osrd/standalone_sim/result/ResultTrain.java +++ b/core/src/main/java/fr/sncf/osrd/standalone_sim/result/ResultTrain.java @@ -12,8 +12,6 @@ public class ResultTrain { @Json(name = "head_positions") public final List headPositions; public final List stops; - @Json(name = "route_occupancies") - public final Map routeOccupancies; /** * A signal sighting represents the time and offset at which a train first sees a signal. @@ -160,8 +158,7 @@ public RoutingRequirement withAddedTime(double timeToAdd) { public ResultTrain( List speeds, List headPositions, - List stops, Map routeOccupancies, + List stops, double mechanicalEnergyConsumed, List signalSightings, List zoneUpdates, @@ -170,7 +167,6 @@ public ResultTrain( this.speeds = speeds; this.headPositions = headPositions; this.stops = stops; - this.routeOccupancies = routeOccupancies; this.mechanicalEnergyConsumed = mechanicalEnergyConsumed; this.signalSightings = signalSightings; this.zoneUpdates = zoneUpdates; @@ -184,7 +180,6 @@ public ResultTrain withDepartureTime(Double departureTime) { speeds.stream().map(speed -> speed.withAddedTime(departureTime)).toList(), headPositions.stream().map(pos -> pos.withAddedTime(departureTime)).toList(), stops.stream().map(stop -> stop.withAddedTime(departureTime)).toList(), - Maps.transformValues(routeOccupancies, occ -> occ.withAddedTime(departureTime)), mechanicalEnergyConsumed, signalSightings.stream().map(sighting -> sighting.withAddedTime(departureTime)).toList(), zoneUpdates.stream().map(update -> update.withAddedTime(departureTime)).toList(), diff --git a/core/src/main/java/fr/sncf/osrd/stdcm/graph/STDCMGraph.java b/core/src/main/java/fr/sncf/osrd/stdcm/graph/STDCMGraph.java index c2837707593..ec61397531c 100644 --- a/core/src/main/java/fr/sncf/osrd/stdcm/graph/STDCMGraph.java +++ b/core/src/main/java/fr/sncf/osrd/stdcm/graph/STDCMGraph.java @@ -93,7 +93,7 @@ public Collection getAdjacentEdges(STDCMNode node) { .makeAllEdges(); else { var res = new ArrayList(); - var neighbors = blockInfra.getBlocksAtDetector(node.detector()); + var neighbors = blockInfra.getBlocksStartingAtDetector(node.detector()); for (var neighbor : toIntList(neighbors)) { res.addAll( STDCMEdgeBuilder.fromNode(this, node, neighbor) diff --git a/core/src/main/java/fr/sncf/osrd/stdcm/preprocessing/implementation/UnavailableSpaceBuilder.java b/core/src/main/java/fr/sncf/osrd/stdcm/preprocessing/implementation/UnavailableSpaceBuilder.java index acf41561a5c..e05a883a7e1 100644 --- a/core/src/main/java/fr/sncf/osrd/stdcm/preprocessing/implementation/UnavailableSpaceBuilder.java +++ b/core/src/main/java/fr/sncf/osrd/stdcm/preprocessing/implementation/UnavailableSpaceBuilder.java @@ -4,18 +4,20 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import fr.sncf.osrd.api.stdcm.STDCMRequest; +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; import fr.sncf.osrd.envelope_sim.PhysicsRollingStock; import fr.sncf.osrd.sim_infra.api.BlockInfra; -import fr.sncf.osrd.sim_infra.api.InterlockingInfraKt; import fr.sncf.osrd.sim_infra.api.RawSignalingInfra; -import fr.sncf.osrd.sim_infra.api.Route; -import fr.sncf.osrd.sim_infra.utils.BlockRecoveryKt; +import fr.sncf.osrd.sim_infra.impl.BlockInfraImplKt; +import fr.sncf.osrd.standalone_sim.result.ResultTrain; import fr.sncf.osrd.stdcm.OccupancySegment; -import fr.sncf.osrd.utils.indexing.MutableStaticIdxArrayList; import fr.sncf.osrd.utils.units.Distance; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; public class UnavailableSpaceBuilder { @@ -29,120 +31,95 @@ public class UnavailableSpaceBuilder { * This is the first step to compute STDCM, the goal is to get rid of railway rules and extra complexity * as soon as possible. After this step we can look for a single curve that avoids unavailable segment. */ public static Multimap computeUnavailableSpace( - RawSignalingInfra rawInfra, + RawSignalingInfra infra, BlockInfra blockInfra, - Collection occupancies, + Collection spacingRequirements, PhysicsRollingStock rollingStock, double marginToAddBeforeEachBlock, double marginToAddAfterEachBlock ) { - Multimap unavailableSpace = HashMultimap.create(); - for (var occupancy : occupancies) { - var route = rawInfra.getRouteFromName(occupancy.id); - var length = rawInfra.getRouteLength(route); - var timeStart = occupancy.startOccupancyTime - marginToAddBeforeEachBlock; - var timeEnd = occupancy.endOccupancyTime + marginToAddAfterEachBlock; + var blockUse = buildBlockUse(infra, blockInfra, spacingRequirements, + marginToAddBeforeEachBlock, marginToAddAfterEachBlock); - //Generating current block occupancy - addRouteOccupancy(unavailableSpace, rawInfra, blockInfra, route, timeStart, timeEnd, 0, length); + for (var blockEntry : blockUse.entrySet()) { + var blockId = blockEntry.getKey(); + var blockLength = blockInfra.getBlockLength(blockId); + var useTimes = blockEntry.getValue(); + for (var timeRange : useTimes.asRanges()) { - //Generating sight Distance occupancy - var predecessorRoutes = getPreviousRoutes(rawInfra, route); - for (var predecessorRoute : predecessorRoutes) { - var preBlockLength = rawInfra.getRouteLength(predecessorRoute); - addRouteOccupancy( - unavailableSpace, - rawInfra, - blockInfra, - predecessorRoute, - timeStart, - timeEnd, - Math.max(0, preBlockLength - SIGHT_DISTANCE), - preBlockLength - ); - var previousBlock = new OccupancySegment( - timeStart, - timeEnd, - Math.max(0, preBlockLength - SIGHT_DISTANCE), - preBlockLength + // Generate current block occupancy + unavailableSpace.put(blockId, new OccupancySegment( + timeRange.lowerEndpoint(), timeRange.upperEndpoint(), 0, blockLength) ); - unavailableSpace.put(predecessorRoute, previousBlock); - } - //Generating successorRoute occupancy - var successorRoutes = getNextRoutes(rawInfra, route); - for (var successorRoute : successorRoutes) { - var successorBlockLength = rawInfra.getRouteLength(successorRoute); - addRouteOccupancy( - unavailableSpace, - rawInfra, - blockInfra, - successorRoute, - timeStart, - timeEnd, - 0, - successorBlockLength - ); - } + // Generate the warnings in blocks before the ones used by other trains + var predecessorBlocks = getPreviousBlocks(infra, blockInfra, blockId); + for (var predecessorBlock : predecessorBlocks) { + var preBlockLength = blockInfra.getBlockLength(predecessorBlock); + unavailableSpace.put(predecessorBlock, new OccupancySegment( + timeRange.lowerEndpoint(), timeRange.upperEndpoint(), 0, preBlockLength + )); - //Generating rollingStock length occupancy - for (var successorRoute : successorRoutes) { - var secondSuccessorRoutes = getNextRoutes(rawInfra, successorRoute); - for (var secondSuccessorRoute : secondSuccessorRoutes) { - var end = Math.min( - Distance.fromMeters(rollingStock.getLength()), - rawInfra.getRouteLength(secondSuccessorRoute) - ); - addRouteOccupancy(unavailableSpace, rawInfra, blockInfra, secondSuccessorRoute, - timeStart, timeEnd, 0, end); + // Generate the sight distance requirements in the blocks before that + for (var secondPredecessorBlock : getPreviousBlocks(infra, blockInfra, predecessorBlock)) { + var secPreBlockLength = blockInfra.getBlockLength(secondPredecessorBlock); + unavailableSpace.put(secondPredecessorBlock, new OccupancySegment( + timeRange.lowerEndpoint(), timeRange.upperEndpoint(), + Math.max(0, secPreBlockLength - SIGHT_DISTANCE), preBlockLength + )); + } + } + + // Generate train length occupancy + var successorBlocks = getNextBlocks(infra, blockInfra, blockId); + for (var successorBlock : successorBlocks) { + var nextBlockLength = blockInfra.getBlockLength(successorBlock); + unavailableSpace.put(successorBlock, new OccupancySegment( + timeRange.lowerEndpoint(), timeRange.upperEndpoint(), + 0, Math.min(nextBlockLength, Distance.fromMeters(rollingStock.getLength())) + )); } } } return unavailableSpace; } - /** Sets the occupancy for a route interval, adding entries to any matching block. - * This is a significant oversimplification, but it keeps the same behavior as before the kt infra migration, - * making testing easier. This whole class is to be deleted when moving to a more accurate behavior. */ - private static void addRouteOccupancy( - Multimap res, + /** Builds the time during which the blocks are used by another train (including a forward signal cascade) + * This step is also used to merge together the small overlapping time intervals + * across different zones or trains. */ + private static Map> buildBlockUse( RawSignalingInfra infra, BlockInfra blockInfra, - int route, - double timeStart, - double timeEnd, - long distanceStart, - long distanceEnd + Collection requirements, + double marginToAddBeforeEachBlock, + double marginToAddAfterEachBlock ) { - var routeList = new MutableStaticIdxArrayList(); - routeList.add(route); - for (var blockPath : BlockRecoveryKt.recoverBlocks(infra, blockInfra, routeList, null)) { - var blockList = BlockRecoveryKt.toBlockList(blockPath); - long routeOffset = 0; - for (var blockId : toIntList(blockList)) { - var blockLength = blockInfra.getBlockLength(blockId); - var start = Math.max(0, distanceStart - routeOffset); - var end = Math.min(blockLength, distanceEnd - routeOffset); - if (start < end) { - var newSegment = new OccupancySegment(timeStart, timeEnd, start, end); - res.put(blockId, newSegment); - } - routeOffset += blockLength; + var res = new HashMap>(); + for (var requirement : requirements) { + var zoneId = infra.getZoneFromName(requirement.zone); + var timeRange = Range.closed( + requirement.beginTime - marginToAddBeforeEachBlock, + requirement.endTime + marginToAddAfterEachBlock + ); + for (var blockId : toIntList(blockInfra.getBlocksInZone(zoneId))) { + res.putIfAbsent(blockId, TreeRangeSet.create()); + res.get(blockId).add(timeRange); } } + return res; } - /** Returns the routes that lead into the given one */ - private static Set getPreviousRoutes(RawSignalingInfra infra, int route) { - var entry = InterlockingInfraKt.getRouteEntry(infra, route); - return new HashSet<>(toIntList(infra.getRoutesEndingAtDet(entry))); + /** Returns the blocks that lead into the given one */ + private static Set getPreviousBlocks(RawSignalingInfra infra, BlockInfra blockInfra, int blockId) { + var entry = BlockInfraImplKt.getBlockEntry(blockInfra, infra, blockId); + return new HashSet<>(toIntList(blockInfra.getBlocksEndingAtDetector(entry))); } - /** Returns the routes that follow the given one */ - private static Set getNextRoutes(RawSignalingInfra infra, int route) { - var exit = InterlockingInfraKt.getRouteExit(infra, route); - return new HashSet<>(toIntList(infra.getRoutesStartingAtDet(exit))); + /** Returns the blocks that follow the given one */ + private static Set getNextBlocks(RawSignalingInfra infra, BlockInfra blockInfra, int blockId) { + var entry = BlockInfraImplKt.getBlockExit(blockInfra, infra, blockId); + return new HashSet<>(toIntList(blockInfra.getBlocksStartingAtDetector(entry))); } } diff --git a/core/src/main/java/fr/sncf/osrd/utils/graph/GraphAdapter.java b/core/src/main/java/fr/sncf/osrd/utils/graph/GraphAdapter.java index cc1ee17c69d..bdac31abc42 100644 --- a/core/src/main/java/fr/sncf/osrd/utils/graph/GraphAdapter.java +++ b/core/src/main/java/fr/sncf/osrd/utils/graph/GraphAdapter.java @@ -28,7 +28,7 @@ public Integer getEdgeEnd(Integer blockId) { /** Returns all the edges (blocks) that start at the given node (detector) */ @Override public Collection getAdjacentEdges(Integer detectorId) { - var neighborBlocks = blockInfra.getBlocksAtDetector(detectorId); + var neighborBlocks = blockInfra.getBlocksStartingAtDetector(detectorId); return KtToJavaConverter.toIntList(neighborBlocks); } } diff --git a/core/src/test/java/fr/sncf/osrd/api/StandaloneSimulationTest.java b/core/src/test/java/fr/sncf/osrd/api/StandaloneSimulationTest.java index 81882342f77..ec976274b85 100644 --- a/core/src/test/java/fr/sncf/osrd/api/StandaloneSimulationTest.java +++ b/core/src/test/java/fr/sncf/osrd/api/StandaloneSimulationTest.java @@ -129,7 +129,6 @@ public void simple() throws Exception { var speeds = trainResult.speeds.toArray(new ResultSpeed[0]); for (int i = 1; i < speeds.length; i++) assertTrue(speeds[i - 1].position <= speeds[i].position); - assertEquals(7, trainResult.routeOccupancies.size()); // check mrsp var mrsp = simResult.speedLimits.get(0); diff --git a/core/src/test/java/fr/sncf/osrd/stdcm/FullSTDCMTests.java b/core/src/test/java/fr/sncf/osrd/stdcm/FullSTDCMTests.java index dafcd73f338..691593d4e3b 100644 --- a/core/src/test/java/fr/sncf/osrd/stdcm/FullSTDCMTests.java +++ b/core/src/test/java/fr/sncf/osrd/stdcm/FullSTDCMTests.java @@ -1,14 +1,23 @@ package fr.sncf.osrd.stdcm; import static fr.sncf.osrd.Helpers.getExampleRollingStock; +import static fr.sncf.osrd.api.stdcm.STDCMEndpoint.makeTrainSchedule; +import static fr.sncf.osrd.train.TestTrains.REALISTIC_FAST_TRAIN; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.common.collect.HashMultimap; import fr.sncf.osrd.Helpers; +import fr.sncf.osrd.api.FullInfra; import fr.sncf.osrd.railjson.parser.RJSRollingStockParser; +import fr.sncf.osrd.standalone_sim.ScheduleMetadataExtractor; +import fr.sncf.osrd.standalone_sim.result.ResultTrain; +import fr.sncf.osrd.train.RollingStock; import fr.sncf.osrd.utils.units.Distance; import org.junit.jupiter.api.Test; import java.io.IOException; import java.net.URISyntaxException; +import java.util.List; import java.util.Set; public class FullSTDCMTests { @@ -42,17 +51,22 @@ public void testTinyInfraSmallOpening() throws IOException, URISyntaxException { "rt.buffer_stop_b->tde.foo_b-switch_foo", Distance.fromMeters(100))); var end = Set.of(Helpers.convertRouteLocation(infra, "rt.tde.foo_b-switch_foo->buffer_stop_c", Distance.fromMeters(10125))); - var occupancies = STDCMHelpers.makeOccupancyFromPath(infra, start, end, 0); + var requirements = STDCMHelpers.makeRequirementsFromPath(infra, start, end, 0); + var occupancies = STDCMHelpers.makeOccupancyFromRequirements(infra, requirements); double minDelay = STDCMHelpers.getMaxOccupancyLength(occupancies); // Eventually we may need to add a % margin - occupancies.putAll(STDCMHelpers.makeOccupancyFromPath(infra, start, end, minDelay * 2)); + occupancies.putAll(STDCMHelpers.makeOccupancyFromRequirements(infra, + STDCMHelpers.makeRequirementsFromPath(infra, start, end, minDelay * 2) + )); var res = new STDCMPathfindingBuilder() .setInfra(infra) .setStartTime(minDelay) .setStartLocations(start) .setEndLocations(end) .setUnavailableTimes(occupancies) + .setMaxDepartureDelay(minDelay * 2) .run(); assertNotNull(res); + checkNoConflict(infra, requirements, res); } /** We try to fit a train in a short opening between two trains, this time on small_infra */ @@ -63,15 +77,65 @@ public void testSmallInfraSmallOpening() throws IOException, URISyntaxException "rt.buffer_stop.3->DB0", Distance.fromMeters(1590))); var end = Set.of(Helpers.convertRouteLocation(infra, "rt.DH2->buffer_stop.7", Distance.fromMeters(5000))); - var occupancies = STDCMHelpers.makeOccupancyFromPath(infra, start, end, 0); - occupancies.putAll(STDCMHelpers.makeOccupancyFromPath(infra, start, end, 600)); + var requirements = STDCMHelpers.makeRequirementsFromPath(infra, start, end, 0); + requirements.addAll(STDCMHelpers.makeRequirementsFromPath(infra, start, end, 600)); var res = new STDCMPathfindingBuilder() .setInfra(infra) .setStartTime(300) .setStartLocations(start) .setEndLocations(end) - .setUnavailableTimes(occupancies) + .setUnavailableTimes(STDCMHelpers.makeOccupancyFromRequirements(infra, requirements)) + .setMaxDepartureDelay(600) .run(); assertNotNull(res); + checkNoConflict(infra, requirements, res); + } + + /** We make an opening that is just too small to fit a train, + * we check that it isn't taken and doesn't cause conflicts */ + @Test + public void testSmallInfraImpossibleOpening() throws IOException, URISyntaxException { + var infra = Helpers.fullInfraFromRJS(Helpers.getExampleInfra("small_infra/infra.json")); + var start = Set.of(Helpers.convertRouteLocation(infra, + "rt.buffer_stop.3->DB0", Distance.fromMeters(1590))); + var end = Set.of(Helpers.convertRouteLocation(infra, + "rt.DH2->buffer_stop.7", Distance.fromMeters(5000))); + var requirements = STDCMHelpers.makeRequirementsFromPath(infra, start, end, 0); + var occupancies = STDCMHelpers.makeOccupancyFromRequirements(infra, requirements); + double minDelay = STDCMHelpers.getMaxOccupancyLength(occupancies); + occupancies.putAll(STDCMHelpers.makeOccupancyFromRequirements(infra, + STDCMHelpers.makeRequirementsFromPath(infra, start, end, minDelay * 0.95) + )); + var res = new STDCMPathfindingBuilder() + .setInfra(infra) + .setStartLocations(start) + .setEndLocations(end) + .setUnavailableTimes(STDCMHelpers.makeOccupancyFromRequirements(infra, requirements)) + .run(); + assertNotNull(res); + checkNoConflict(infra, requirements, res); + } + + /** Check that the result we find doesn't cause a conflict */ + private void checkNoConflict(FullInfra infra, List requirements, STDCMResult res) { + var requirementMap = HashMultimap.create(); + for (var requirement : requirements) { + requirementMap.put(requirement.zone, requirement); + } + var newRequirements = ScheduleMetadataExtractor.run( + res.envelope(), + res.trainPath(), + res.chunkPath(), + makeTrainSchedule(res.envelope().getEndPos(), REALISTIC_FAST_TRAIN, + RollingStock.Comfort.STANDARD, res.stopResults()), + infra + ).spacingRequirements; + for (var requirement : newRequirements) { + var shifted = requirement.withAddedTime(res.departureTime()); + for (var existingRequirement : requirementMap.get(requirement.zone)) { + assertTrue(shifted.beginTime >= existingRequirement.endTime + || shifted.endTime <= existingRequirement.beginTime); + } + } } } diff --git a/core/src/test/java/fr/sncf/osrd/stdcm/STDCMHelpers.java b/core/src/test/java/fr/sncf/osrd/stdcm/STDCMHelpers.java index 521c757fd2f..5ba038e1a11 100644 --- a/core/src/test/java/fr/sncf/osrd/stdcm/STDCMHelpers.java +++ b/core/src/test/java/fr/sncf/osrd/stdcm/STDCMHelpers.java @@ -7,13 +7,13 @@ import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import fr.sncf.osrd.DriverBehaviour; -import fr.sncf.osrd.api.FullInfra; -import fr.sncf.osrd.api.stdcm.STDCMRequest; +import fr.sncf.osrd.api.*; import fr.sncf.osrd.api.utils.PathPropUtils; import fr.sncf.osrd.envelope_sim_infra.EnvelopeTrainPath; import fr.sncf.osrd.sim_infra.impl.ChunkPath; import fr.sncf.osrd.standalone_sim.EnvelopeStopWrapper; import fr.sncf.osrd.standalone_sim.StandaloneSim; +import fr.sncf.osrd.standalone_sim.result.ResultTrain; import fr.sncf.osrd.stdcm.graph.STDCMSimulations; import fr.sncf.osrd.stdcm.preprocessing.implementation.UnavailableSpaceBuilder; import fr.sncf.osrd.train.RollingStock; @@ -27,8 +27,10 @@ import java.util.Set; public class STDCMHelpers { - /** Make the occupancy multimap of a train going from point A to B starting at departureTime */ - public static Multimap makeOccupancyFromPath( + /** + * Make the occupancy multimap of a train going from point A to B starting at departureTime + */ + public static List makeRequirementsFromPath( FullInfra infra, Set> startLocations, Set> endLocations, @@ -55,19 +57,27 @@ public static Multimap makeOccupancyFromPath( 2., new DriverBehaviour(0, 0) ); - var rawOccupancies = result.baseSimulations.get(0).routeOccupancies; - var occupancies = new ArrayList(); - for (var entry : rawOccupancies.entrySet()) { - occupancies.add(new STDCMRequest.RouteOccupancy( - entry.getKey(), - departureTime + entry.getValue().timeHeadOccupy, - departureTime + entry.getValue().timeTailFree + var rawOccupancies = result.baseSimulations.get(0).spacingRequirements; + var requirements = new ArrayList(); + for (var entry : rawOccupancies) { + requirements.add(new ResultTrain.SpacingRequirement( + entry.zone, + departureTime + entry.beginTime, + departureTime + entry.endTime )); } + return requirements; + } + + /** Make the route occupancies from a spacing requirement list */ + public static Multimap makeOccupancyFromRequirements( + FullInfra infra, + List requirements + ) { return UnavailableSpaceBuilder.computeUnavailableSpace( infra.rawInfra(), infra.blockInfra(), - occupancies, + requirements, REALISTIC_FAST_TRAIN, 0, 0 diff --git a/core/src/test/java/fr/sncf/osrd/stdcm/preprocessing/BlockAvailabilityLegacyAdapterTests.java b/core/src/test/java/fr/sncf/osrd/stdcm/preprocessing/BlockAvailabilityLegacyAdapterTests.java index e4cdca88fa0..0ef987a45cd 100644 --- a/core/src/test/java/fr/sncf/osrd/stdcm/preprocessing/BlockAvailabilityLegacyAdapterTests.java +++ b/core/src/test/java/fr/sncf/osrd/stdcm/preprocessing/BlockAvailabilityLegacyAdapterTests.java @@ -5,11 +5,11 @@ import static fr.sncf.osrd.stdcm.STDCMHelpers.meters; import static org.junit.jupiter.api.Assertions.assertEquals; -import com.google.common.collect.Iterables; -import fr.sncf.osrd.api.stdcm.STDCMRequest; import fr.sncf.osrd.envelope.Envelope; import fr.sncf.osrd.envelope.part.EnvelopePart; import fr.sncf.osrd.envelope_sim.SimpleRollingStock; +import fr.sncf.osrd.sim_infra.api.InterlockingInfraKt; +import fr.sncf.osrd.standalone_sim.result.ResultTrain; import fr.sncf.osrd.stdcm.preprocessing.implementation.BlockAvailabilityLegacyAdapter; import fr.sncf.osrd.stdcm.preprocessing.implementation.UnavailableSpaceBuilder; import fr.sncf.osrd.stdcm.preprocessing.interfaces.BlockAvailabilityInterface; @@ -28,6 +28,9 @@ public void simpleUnavailableRouteTest() { "rt.DA6->DC6" ); var blocks = getBlocksOnRoutes(infra, routes); + var zone = InterlockingInfraKt.getZonePathZone(infra.rawInfra(), + infra.blockInfra().getBlockPath(blocks.get(0)).get(0)); + var zoneName = infra.rawInfra().getZoneName(zone); double startOccupancy = 42; double endOccupancy = 84; @@ -35,8 +38,8 @@ public void simpleUnavailableRouteTest() { var unavailableSpace = UnavailableSpaceBuilder.computeUnavailableSpace( infra.rawInfra(), infra.blockInfra(), - List.of(new STDCMRequest.RouteOccupancy( - routes.get(1), + List.of(new ResultTrain.SpacingRequirement( + zoneName, startOccupancy, endOccupancy )), @@ -46,7 +49,7 @@ public void simpleUnavailableRouteTest() { ); var adapter = new BlockAvailabilityLegacyAdapter(infra.blockInfra(), unavailableSpace); var res = adapter.getAvailability( - List.of(Iterables.getLast(blocks)), + List.of(blocks.get(0)), 0, meters(1), Envelope.make(EnvelopePart.generateTimes(new double[]{0., 1.}, new double[]{100., 100.})), diff --git a/core/src/test/java/fr/sncf/osrd/stdcm/preprocessing/UnavailableSpaceBuilderTests.java b/core/src/test/java/fr/sncf/osrd/stdcm/preprocessing/UnavailableSpaceBuilderTests.java index 37cd5663754..55f59742a90 100644 --- a/core/src/test/java/fr/sncf/osrd/stdcm/preprocessing/UnavailableSpaceBuilderTests.java +++ b/core/src/test/java/fr/sncf/osrd/stdcm/preprocessing/UnavailableSpaceBuilderTests.java @@ -8,6 +8,7 @@ import fr.sncf.osrd.Helpers; import fr.sncf.osrd.api.stdcm.STDCMRequest; +import fr.sncf.osrd.standalone_sim.result.ResultTrain; import fr.sncf.osrd.stdcm.OccupancySegment; import fr.sncf.osrd.utils.DummyInfra; import org.junit.jupiter.api.Test; @@ -30,7 +31,7 @@ public void testFirstBlockOccupied() { var res = computeUnavailableSpace( infra, infra, - Set.of(new STDCMRequest.RouteOccupancy("a->b", 0, 100)), + Set.of(new ResultTrain.SpacingRequirement("a->b", 0, 100)), REALISTIC_FAST_TRAIN, 0, 0 @@ -43,12 +44,9 @@ public void testFirstBlockOccupied() { ); assertEquals( Set.of( - // If the train is in this area, the previous block would be "yellow", causing a conflict - new OccupancySegment(0, 100, 0, meters(1000)) - - // Margin added to the base occupancy to account for the train length, - // it can be removed if this test fails as it overlaps with the previous one - //new OccupancySegment(0, 100, 0, REALISTIC_FAST_TRAIN.getLength()) + // The train needs to have fully cleared the first block, + // its head can't be in the beginning of the second block + new OccupancySegment(0, 100, 0, meters(REALISTIC_FAST_TRAIN.getLength())) ), res.get(secondBlock) ); @@ -62,15 +60,15 @@ public void testSecondBlockOccupied() { var res = computeUnavailableSpace( infra, infra, - Set.of(new STDCMRequest.RouteOccupancy("b->c", 0, 100)), + Set.of(new ResultTrain.SpacingRequirement("b->c", 0, 100)), REALISTIC_FAST_TRAIN, 0, 0 ); assertEquals( Set.of( - // Entering this area would cause the train to see a signal that isn't green - new OccupancySegment(0, 100, meters(1000 - 400), meters(1000)) + // This block would display a warning and wouldn't be available + new OccupancySegment(0, 100, 0, meters(1000)) ), res.get(firstBlock) ); @@ -101,7 +99,7 @@ public void testBranchingBlocks() { final var res = computeUnavailableSpace( infra, infra, - Set.of(new STDCMRequest.RouteOccupancy("a1->center", 0, 100)), + Set.of(new ResultTrain.SpacingRequirement("center->b1", 0, 100)), REALISTIC_FAST_TRAIN, 0, 0 @@ -110,48 +108,43 @@ public void testBranchingBlocks() { Set.of( new OccupancySegment(0, 100, 0, meters(1000)) // base occupancy ), - res.get(a1) + res.get(b1) ); assertEquals( Set.of(), - res.get(a2) + res.get(b2) ); assertEquals( Set.of( - // If the train is in this area, the previous block would be "yellow", causing a conflict + // The previous block would display a warning new OccupancySegment(0, 100, 0, meters(1000)) - - // Margin added to the base occupancy to account for the train length, - // it can be removed if this test fails as it overlaps with the previous one - // new OccupancySegment(0, 100, 0, REALISTIC_FAST_TRAIN.getLength()) ), res.get(b1) ); - assertEquals(res.get(b1), res.get(b2)); + assertEquals(res.get(a1), res.get(a2)); } @Test public void testThirdBlock() { var infra = DummyInfra.make(); - infra.addBlock("a", "b", meters(1000)); + var firstBlock = infra.addBlock("a", "b", meters(1000)); infra.addBlock("b", "c", meters(1000)); - var thirdBlock = infra.addBlock("c", "d", meters(1000)); + infra.addBlock("c", "d", meters(1000)); var res = computeUnavailableSpace( infra, infra, - Set.of(new STDCMRequest.RouteOccupancy("a->b", 0, 100)), + Set.of(new ResultTrain.SpacingRequirement("c->d", 0, 100)), REALISTIC_FAST_TRAIN, 0, 0 ); assertEquals( Set.of( - // The second block can't be occupied in that time because it would cause a "yellow" state - // in the first one (conflict), and this accounts for the extra margin needed in the third - // block caused by the train length - new OccupancySegment(0, 100, 0, meters(REALISTIC_FAST_TRAIN.getLength())) + // Second block displays a warning, first block can't be used in the area where + // the signal of the second block is visible + new OccupancySegment(0, 100, meters(1000 - 400), meters(1000)) ), - res.get(thirdBlock) + res.get(firstBlock) ); } @@ -163,7 +156,7 @@ public void testGridMargins() { var res = computeUnavailableSpace( infra, infra, - Set.of(new STDCMRequest.RouteOccupancy("a->b", 100, 200)), + Set.of(new ResultTrain.SpacingRequirement("b->c", 100, 200)), REALISTIC_FAST_TRAIN, 20, 60 diff --git a/core/src/test/kotlin/fr/sncf/osrd/utils/DummyInfra.kt b/core/src/test/kotlin/fr/sncf/osrd/utils/DummyInfra.kt index 443b3aa8175..7dcf1bfdca1 100644 --- a/core/src/test/kotlin/fr/sncf/osrd/utils/DummyInfra.kt +++ b/core/src/test/kotlin/fr/sncf/osrd/utils/DummyInfra.kt @@ -271,6 +271,14 @@ class DummyInfra : RawInfra, BlockInfra { TODO("Not yet implemented") } + override fun getZoneName(zone: ZoneId): String { + return getRouteName(RouteId(zone.index)) + } + + override fun getZoneFromName(name: String): ZoneId { + return ZoneId(getRouteFromName(name).index) + } + override val detectors: StaticIdxSpace get() = TODO("Not yet implemented") @@ -434,6 +442,10 @@ class DummyInfra : RawInfra, BlockInfra { return makeIndexList(block) } + override fun getBlocksInZone(zone: ZoneId): StaticIdxList { + return mutableStaticIdxArrayListOf(BlockId(zone.index)) + } + override fun getBlockSignals(block: BlockId): StaticIdxList { TODO("Not yet implemented") } @@ -450,13 +462,20 @@ class DummyInfra : RawInfra, BlockInfra { TODO("Not yet implemented") } - override fun getBlocksAtDetector(detector: DirDetectorId): StaticIdxList { + override fun getBlocksStartingAtDetector(detector: DirDetectorId): StaticIdxList { val res = mutableStaticIdxArrayListOf() for (x in entryMap.get(detector)) res.add(x) return res } + override fun getBlocksEndingAtDetector(detector: DirDetectorId): StaticIdxList { + val res = mutableStaticIdxArrayListOf() + for (id in getRoutesEndingAtDet(detector)) + res.add(BlockId(id.index)) + return res + } + override fun getBlocksAtSignal(signal: LogicalSignalId): StaticIdxList { TODO("Not yet implemented") } From c321c20e19f5779cbe41feff5b196c6e316e90a2 Mon Sep 17 00:00:00 2001 From: Younes Khoudli Date: Mon, 9 Oct 2023 15:34:41 +0200 Subject: [PATCH 2/2] editoast: adapt to the new stdcm endpoint --- editoast/src/core/stdcm.rs | 11 +--- editoast/src/models/train_schedule.rs | 1 - editoast/src/views/stdcm/mod.rs | 79 +++++++++++---------------- editoast/src/views/timetable.rs | 68 ++++++++++++----------- 4 files changed, 69 insertions(+), 90 deletions(-) diff --git a/editoast/src/core/stdcm.rs b/editoast/src/core/stdcm.rs index 2047bdcb387..9e014750836 100644 --- a/editoast/src/core/stdcm.rs +++ b/editoast/src/core/stdcm.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::models::RollingStockModel; +use crate::models::{RollingStockModel, SpacingRequirement}; use crate::core::{pathfinding::Waypoint, simulation::SimulationResponse}; use crate::schema::rolling_stock::RollingStockComfortType; @@ -15,7 +15,7 @@ pub struct STDCMCoreRequest { pub expected_version: String, pub rolling_stock: RollingStockModel, pub comfort: RollingStockComfortType, - pub route_occupancies: Vec, + pub spacing_requirements: Vec, pub steps: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub start_time: Option, @@ -43,13 +43,6 @@ pub struct STDCMCoreStep { pub waypoints: Vec, } -#[derive(Serialize, Deserialize, Debug)] -pub struct STDCMCoreRouteOccupancy { - pub id: String, - pub start_occupancy_time: f64, - pub end_occupancy_time: f64, -} - #[derive(Serialize, Deserialize, Debug)] pub struct STDCMCoreResponse { pub simulation: SimulationResponse, diff --git a/editoast/src/models/train_schedule.rs b/editoast/src/models/train_schedule.rs index dd0ae5de6c9..504b590bd6f 100644 --- a/editoast/src/models/train_schedule.rs +++ b/editoast/src/models/train_schedule.rs @@ -265,7 +265,6 @@ pub struct ResultTrain { pub speeds: Vec, pub head_positions: Vec, pub stops: Vec, - pub route_occupancies: HashMap, pub mechanical_energy_consumed: f64, pub signal_sightings: Vec, pub zone_updates: Vec, diff --git a/editoast/src/views/stdcm/mod.rs b/editoast/src/views/stdcm/mod.rs index dc7fcfce78b..4308cbb5e7c 100644 --- a/editoast/src/views/stdcm/mod.rs +++ b/editoast/src/views/stdcm/mod.rs @@ -1,12 +1,9 @@ -use crate::core::stdcm::{ - STDCMCoreRequest, STDCMCoreResponse, STDCMCoreRouteOccupancy, STDCMCoreStep, -}; +use crate::core::stdcm::{STDCMCoreRequest, STDCMCoreResponse, STDCMCoreStep}; use crate::core::{AsCoreRequest, CoreClient}; -use crate::diesel::BelongingToDsl; + use crate::error::Result; -use crate::models::train_schedule::filter_invalid_trains; pub use crate::models::train_schedule::AllowanceValue; -use crate::models::{Create, SimulationOutput}; +use crate::models::{Create, SpacingRequirement}; use crate::models::{ CurveGraph, Infra, PathWaypoint, Pathfinding, PathfindingChangeset, PathfindingPayload, Retrieve, SlopeGraph, TrainSchedule, @@ -16,7 +13,7 @@ use crate::DbPool; use actix_web::dev::HttpServiceFactory; use actix_web::web::{self, Data, Json}; use actix_web::{post, HttpResponse, Responder}; -use diesel_async::RunQueryDsl; + use editoast_derive::EditoastError; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -24,6 +21,7 @@ use thiserror::Error; use super::pathfinding::{ fetch_pathfinding_payload_track_map, parse_pathfinding_payload_waypoints, StepPayload, }; +use super::timetable::get_simulated_schedules_from_timetable; use super::train_schedule::process_simulation_response; use super::train_schedule::projection::Projection; use super::train_schedule::simulation_report::{create_simulation_report, SimulationReport}; @@ -153,15 +151,14 @@ async fn call_core_stdcm( let infra_version = infra.clone().version.unwrap(); let rolling_stock = retrieve_existing_rolling_stock(&db_pool, data.rolling_stock_id).await?; let steps = parse_stdcm_steps(db_pool.clone(), data, &infra).await?; - let route_occupancies = - make_route_occupancies(infra_version.clone(), db_pool, data.timetable_id).await?; + let spacing_requirements = make_spacing_requirements(db_pool, data.timetable_id).await?; STDCMCoreRequest { infra: infra.id.unwrap().to_string(), expected_version: infra_version, rolling_stock, comfort: data.comfort.clone(), steps, - route_occupancies, + spacing_requirements, // end_time is not used by backend currently // but at least one must be defined start_time: data.start_time, @@ -207,47 +204,33 @@ async fn parse_stdcm_steps( /// Create route occupancies, adjusted by simulation departure time. /// uses base_simulation by default, or eco_simulation if given -async fn make_route_occupancies( - infra_version: String, +async fn make_spacing_requirements( db_pool: Data, timetable_id: i64, -) -> Result> { - let schedules = filter_invalid_trains(db_pool.clone(), timetable_id, infra_version).await?; - let mut conn = db_pool.get().await?; - let (simulations, schedules): (Vec, Vec) = ( - SimulationOutput::belonging_to(&schedules) - .load::(&mut conn) - .await?, - schedules, - ); - Ok(simulations - .iter() - .filter_map(|simulation| { - if let Some(schedule) = schedules - .iter() - .find(|s| s.id == simulation.train_schedule_id) - { - let sim = simulation - .eco_simulation - .as_ref() - .unwrap_or(&simulation.base_simulation); - Some( - sim.route_occupancies - .iter() - .map(|(route_id, occupancy)| STDCMCoreRouteOccupancy { - id: route_id.to_string(), - start_occupancy_time: occupancy.time_head_occupy - + schedule.departure_time, - end_occupancy_time: occupancy.time_tail_free + schedule.departure_time, - }) - .collect::>(), - ) - } else { - None - } +) -> Result> { + let (schedules, simulations) = + get_simulated_schedules_from_timetable(timetable_id, db_pool).await?; + + let res = simulations + .into_iter() + .zip(schedules) + .flat_map(|(simulation, schedule)| { + let sim = simulation + .eco_simulation + .map(|sim| sim.0) + .unwrap_or(simulation.base_simulation.0); + + sim.spacing_requirements + .into_iter() + .map(|req| SpacingRequirement { + zone: req.zone, + begin_time: req.begin_time + schedule.departure_time, + end_time: req.end_time + schedule.departure_time, + }) + .collect::>() }) - .flatten() - .collect()) + .collect(); + Ok(res) } /// Creates a Pathfinding using the same function used with core /pathfinding response diff --git a/editoast/src/views/timetable.rs b/editoast/src/views/timetable.rs index b702c47fdc7..cfe9705bcaf 100644 --- a/editoast/src/views/timetable.rs +++ b/editoast/src/views/timetable.rs @@ -95,6 +95,40 @@ struct Conflict { conflict_type: ConflictType, } +pub async fn get_simulated_schedules_from_timetable( + timetable_id: i64, + db_pool: Data, +) -> Result<(Vec, Vec)> { + let mut conn = db_pool.get().await?; + use crate::tables::train_schedule; + use diesel::{BelongingToDsl, ExpressionMethods, GroupedBy, QueryDsl}; + use diesel_async::RunQueryDsl; + + let train_schedules = train_schedule::table + .filter(train_schedule::timetable_id.eq(timetable_id)) + .load::(&mut conn) + .await?; + + SimulationOutput::belonging_to(&train_schedules) + .load::(&mut conn) + .await? + .grouped_by(&train_schedules) + .into_iter() + .zip(train_schedules) + .map(|(mut sim_output, train_schedule)| { + if sim_output.is_empty() { + return Err(TrainScheduleError::UnsimulatedTrainSchedule { + train_schedule_id: train_schedule.id.expect("TrainSchedule should have an id"), + } + .into()); + } + assert!(sim_output.len() == 1); + Ok((train_schedule, sim_output.remove(0))) + }) + .collect::>>() + .map(|v| v.into_iter().unzip()) +} + /// Compute spacing conflicts for a given timetable /// TODO: This should compute itinary conflicts too #[utoipa::path( @@ -114,38 +148,8 @@ async fn get_conflicts( ) -> Result>> { let timetable_id = timetable_id.into_inner(); - let (schedules, simulations): (Vec, Vec) = { - let mut conn = db_pool.get().await?; - use crate::tables::train_schedule; - use diesel::{BelongingToDsl, ExpressionMethods, GroupedBy, QueryDsl}; - use diesel_async::RunQueryDsl; - - let train_schedules = train_schedule::table - .filter(train_schedule::timetable_id.eq(timetable_id)) - .load::(&mut conn) - .await?; - - SimulationOutput::belonging_to(&train_schedules) - .load::(&mut conn) - .await? - .grouped_by(&train_schedules) - .into_iter() - .zip(train_schedules) - .map(|(mut sim_output, train_schedule)| { - if sim_output.is_empty() { - return Err(TrainScheduleError::UnsimulatedTrainSchedule { - train_schedule_id: train_schedule - .id - .expect("TrainSchedule should have an id"), - } - .into()); - } - assert!(sim_output.len() == 1); - Ok((train_schedule, sim_output.remove(0))) - }) - .collect::>>() - .map(|v| v.into_iter().unzip()) - }?; + let (schedules, simulations) = + get_simulated_schedules_from_timetable(timetable_id, db_pool).await?; let mut id_to_name = HashMap::new(); let mut trains_requirements = Vec::new();