Skip to content

Commit a033171

Browse files
committed
handle multiple consists per train schedule
1 parent c36a1dc commit a033171

File tree

48 files changed

+1164
-336
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1164
-336
lines changed

core/envelope-sim/src/main/java/fr/sncf/osrd/envelope/EnvelopeBuilder.kt

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@ package fr.sncf.osrd.envelope
22

33
import fr.sncf.osrd.envelope.part.EnvelopePart
44

5-
/** Creates an envelope by concatenating envelope parts. Envelope parts must not overlap. */
5+
/** Creates an envelope by concatenating envelope parts. Envelope parts must not overlap. */
66
class EnvelopeBuilder {
77
private var parts: MutableList<EnvelopePart>? = mutableListOf()
88

9-
/** Adds a part to the envelope */
9+
/** Adds a part to the envelope */
1010
fun addPart(part: EnvelopePart) {
1111
checkNotNull(parts) { "build() was already called" }
1212
parts!!.add(part)
1313
}
1414

15-
/** Adds a list of parts */
15+
/** Adds a list of parts */
1616
fun addParts(parts: Array<EnvelopePart>) {
1717
for (part in parts) addPart(part)
1818
}
1919

20-
/** Adds all parts of an envelope */
20+
/** Adds all parts of an envelope */
2121
fun addEnvelope(envelope: Envelope) {
2222
for (part in envelope) addPart(part)
2323
}
2424

25-
/** Reverses the order of the parts */
25+
/** Reverses the order of the parts */
2626
fun reverse() {
2727
checkNotNull(parts) { "build() was already called" }
2828
for (i in 0..<parts!!.size / 2) {
@@ -32,7 +32,7 @@ class EnvelopeBuilder {
3232
}
3333
}
3434

35-
/** Creates a new Envelope */
35+
/** Creates a new Envelope */
3636
fun build(): Envelope {
3737
checkNotNull(parts) { "build() was already called" }
3838
val envelope = Envelope.make(*parts!!.toTypedArray())
@@ -41,11 +41,21 @@ class EnvelopeBuilder {
4141
}
4242

4343
companion object {
44-
/** Concatenates multiple envelopes together */
44+
/** Concatenates multiple envelopes together */
4545
fun concatenate(vararg envelopes: Envelope): Envelope {
4646
val res = EnvelopeBuilder()
4747
for (envelope in envelopes) res.addEnvelope(envelope)
4848
return res.build()
4949
}
5050
}
5151
}
52+
53+
fun concatenateAndShiftEnvelopes(envelopes: List<Envelope>): Envelope {
54+
var currentOffset = 0.0
55+
val envelopeParts = mutableListOf<EnvelopePart>()
56+
for (envelope in envelopes) {
57+
envelopeParts.addAll(envelope.map { it.copyAndShift(currentOffset) })
58+
currentOffset += envelope.endPos
59+
}
60+
return Envelope(envelopeParts.toTypedArray())
61+
}

core/envelope-sim/src/main/java/fr/sncf/osrd/envelope/part/EnvelopePart.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,16 +521,18 @@ class EnvelopePart(
521521
*/
522522
fun copyAndShift(
523523
positionDelta: Double,
524-
minPosition: Double,
525-
maxPosition: Double,
524+
minPosition: Double? = null,
525+
maxPosition: Double? = null,
526526
): EnvelopePart {
527527
val newPositions = DoubleArrayList()
528528
val newSpeeds = DoubleArrayList()
529529
val newTimeDeltas = DoubleArrayList()
530530
newPositions.add(positions[0] + positionDelta)
531531
newSpeeds.add(speeds[0])
532532
for (i in 1 until positions.size) {
533-
val p = max(minPosition, min(maxPosition, positions[i] + positionDelta))
533+
var p = positions[i] + positionDelta
534+
if (minPosition != null) p = max(minPosition, p)
535+
if (maxPosition != null) p = min(maxPosition, p)
534536
if (newPositions.last().value != p) {
535537
// Positions that are an epsilon away may be overlapping after the shift, we only
536538
// add the distinct ones
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package fr.sncf.osrd.path.implementations
2+
3+
import com.google.common.collect.BoundType
4+
import com.google.common.collect.ImmutableRangeMap
5+
import com.google.common.collect.Range
6+
import com.google.common.collect.RangeMap
7+
import fr.sncf.osrd.path.interfaces.Electrification
8+
import fr.sncf.osrd.path.interfaces.PhysicsPath
9+
import fr.sncf.osrd.utils.entries
10+
11+
class SubPhysicsPath(val begin: Double, val end: Double, val path: PhysicsPath) : PhysicsPath {
12+
init {
13+
require(begin in 0.0..<end && end <= path.length)
14+
}
15+
16+
override val length: Double
17+
get() = end - begin
18+
19+
override fun getAverageGrade(begin: Double, end: Double): Double {
20+
require(end <= this.length)
21+
require(begin >= 0)
22+
val newBegin = begin + this.begin
23+
val newEnd = end + this.begin
24+
return path.getAverageGrade(newBegin, newEnd)
25+
}
26+
27+
override fun getMinGrade(begin: Double, end: Double): Double {
28+
require(end <= this.length)
29+
require(begin >= 0)
30+
val newBegin = begin + this.begin
31+
val newEnd = end + this.begin
32+
return path.getMinGrade(newBegin, newEnd)
33+
}
34+
35+
private fun makeRange(
36+
lower: Double,
37+
upper: Double,
38+
type: Pair<BoundType, BoundType>,
39+
): Range<Double> {
40+
return when (type) {
41+
Pair(BoundType.OPEN, BoundType.OPEN) -> Range.open(lower, upper)
42+
Pair(BoundType.OPEN, BoundType.CLOSED) -> Range.openClosed(lower, upper)
43+
Pair(BoundType.CLOSED, BoundType.OPEN) -> Range.closedOpen(lower, upper)
44+
Pair(BoundType.CLOSED, BoundType.CLOSED) -> Range.closed(lower, upper)
45+
else -> throw IllegalArgumentException("Unknown range type: $type")
46+
}
47+
}
48+
49+
override fun getElectrificationMap(
50+
basePowerClass: String?,
51+
powerRestrictionMap: RangeMap<Double, String>?,
52+
powerRestrictionToPowerClass: Map<String, String>?,
53+
ignoreElectricalProfiles: Boolean,
54+
): ImmutableRangeMap<Double, Electrification> {
55+
val powerRestrictionMapBuilder = ImmutableRangeMap.Builder<Double, String>()
56+
powerRestrictionMap?.entries?.forEach { (k, v) ->
57+
powerRestrictionMapBuilder.put(
58+
makeRange(
59+
k.lowerEndpoint() + begin,
60+
k.upperEndpoint() + begin,
61+
Pair(k.lowerBoundType(), k.upperBoundType()),
62+
),
63+
v,
64+
)
65+
}
66+
val newPowerRestrictionMap = powerRestrictionMapBuilder.build()
67+
val electrificationMap =
68+
path.getElectrificationMap(
69+
basePowerClass,
70+
newPowerRestrictionMap,
71+
powerRestrictionToPowerClass,
72+
ignoreElectricalProfiles,
73+
)
74+
val newElectrificationMapBuilder = ImmutableRangeMap.Builder<Double, Electrification>()
75+
electrificationMap.entries.forEach { (k, v) ->
76+
newElectrificationMapBuilder.put(
77+
makeRange(
78+
k.lowerEndpoint() - begin,
79+
k.upperEndpoint() - begin,
80+
Pair(k.lowerBoundType(), k.upperBoundType()),
81+
),
82+
v,
83+
)
84+
}
85+
return newElectrificationMapBuilder.build()
86+
}
87+
}

core/src/main/java/fr/sncf/osrd/cli/BenchSTDCM.kt

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,24 +175,39 @@ class BenchSTDCM : CliCommand {
175175
try {
176176
val temporarySpeedLimitManager =
177177
buildTemporarySpeedLimitManager(infra, request.temporarySpeedLimits)
178-
val rollingStock =
179-
parseRawRollingStock(
180-
request.consistSchedule.values[0].physicsConsist,
181-
request.consistSchedule.values[0].loadingGaugeType,
182-
request.consistSchedule.values[0].supportedSignalingSystems.filter {
183-
// Ignoring ETCS as it is not (yet) supported for STDCM
184-
it != ETCS_LEVEL2.id
185-
},
178+
val consistConfigurations =
179+
request.consistSchedule.values.map { it ->
180+
it.copy(
181+
supportedSignalingSystems =
182+
it.supportedSignalingSystems.filter {
183+
// Ignoring ETCS as it is not (yet) supported for
184+
it != ETCS_LEVEL2.id
185+
}
186+
)
187+
}
188+
val requestConsistSchedule =
189+
request.consistSchedule.copy(values = consistConfigurations)
190+
val allowedTrackSections =
191+
parseTrackSectionIds(infra, request.allowedTrackSections)
192+
val consistSchedules =
193+
ConsistSchedule(
194+
requestConsistSchedule,
195+
infra,
196+
allowedTrackSections,
197+
request.pathItems.size,
186198
)
187199
val steps =
188-
parseSteps(infra, request.pathItems, request.startTime, rollingStock.length)
200+
parseSteps(
201+
infra,
202+
request.pathItems,
203+
request.startTime,
204+
consistSchedules.rollingStocks.map { it.length },
205+
)
189206
val requirements = getRequirements(request, infra, cacheManager)
190-
val allowedTrackSections =
191-
parseTrackSectionIds(infra, request.allowedTrackSections)
192207
path =
193208
findPath(
194209
infra,
195-
rollingStock,
210+
consistSchedules,
196211
request.comfort,
197212
0.0,
198213
steps,

core/src/main/kotlin/fr/sncf/osrd/api/CommonSchemas.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import fr.sncf.osrd.railjson.schema.common.graph.EdgeDirection
66
import fr.sncf.osrd.sim_infra.api.TrackSection
77
import fr.sncf.osrd.utils.DistanceRangeMap
88
import fr.sncf.osrd.utils.distanceRangeMapOf
9+
import fr.sncf.osrd.utils.units.Distance
910
import fr.sncf.osrd.utils.units.Offset
1011
import fr.sncf.osrd.utils.units.TimeDelta
12+
import fr.sncf.osrd.utils.units.meters
1113

1214
data class DirectionalTrackRange(
1315
@Json(name = "track_section") val trackSection: String,
@@ -51,6 +53,16 @@ data class RangeValues<valueT>(
5153
}
5254
return distanceRangeMapOf(rangeMapEntries)
5355
}
56+
57+
fun shifted(distance: Distance): RangeValues<valueT> {
58+
return this.copy(
59+
internalBoundaries =
60+
this.internalBoundaries.map {
61+
require(it + distance > Offset(0.meters))
62+
it + distance
63+
}
64+
)
65+
}
5466
}
5567

5668
class TrackLocation(val track: String, val offset: Offset<TrackSection>)

core/src/main/kotlin/fr/sncf/osrd/api/RollingStockParser.kt

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package fr.sncf.osrd.api
22

33
import fr.sncf.osrd.api.standalone_sim.PhysicsConsistModel
4+
import fr.sncf.osrd.api.stdcm.RequestConsistSchedule
5+
import fr.sncf.osrd.envelope_sim.PhysicsRollingStock
46
import fr.sncf.osrd.envelope_sim.PhysicsRollingStock.TractiveEffortPoint
57
import fr.sncf.osrd.envelope_sim.etcs.toEtcsBrakeParams
8+
import fr.sncf.osrd.graph.PathfindingConstraint
9+
import fr.sncf.osrd.pathfinding.constraints.CachedBlockConstraintCombiner
10+
import fr.sncf.osrd.pathfinding.constraints.initConstraints
611
import fr.sncf.osrd.railjson.schema.rollingstock.RJSEffortCurves.*
712
import fr.sncf.osrd.railjson.schema.rollingstock.RJSLoadingGaugeType
813
import fr.sncf.osrd.railjson.schema.rollingstock.RJSRollingResistance
914
import fr.sncf.osrd.railjson.schema.rollingstock.RJSRollingResistance.Davis
1015
import fr.sncf.osrd.reporting.exceptions.ErrorType
1116
import fr.sncf.osrd.reporting.exceptions.OSRDError
17+
import fr.sncf.osrd.sim_infra.api.TrackSectionId
1218
import fr.sncf.osrd.train.RollingStock
1319
import fr.sncf.osrd.train.RollingStock.*
20+
import kotlin.collections.get
1421

1522
/** Parse the rolling stock model into something the backend can work with */
1623
fun parseRawRollingStock(
@@ -60,6 +67,101 @@ fun parseRawRollingStock(
6067
)
6168
}
6269

70+
/**
71+
* Associates a list of rolling stocks with their related pathfinding constraints. This class
72+
* provides two ways to be built:
73+
* - From a list of STDCM query inputs.
74+
* - From a list of rolling stocks and their pathfinding constraints. This approach is mostly useful
75+
* for testing purposes.
76+
*/
77+
data class ConsistSchedule(
78+
val rollingStocks: List<PhysicsRollingStock>,
79+
val constraints: List<PathfindingConstraint>?,
80+
) {
81+
init {
82+
require(!rollingStocks.isEmpty())
83+
require(constraints == null || rollingStocks.size == constraints.size)
84+
}
85+
86+
companion object {
87+
operator fun invoke(
88+
consistSchedule: RequestConsistSchedule,
89+
infra: FullInfra,
90+
allowedTrackSections: Set<TrackSectionId>? = null,
91+
totalSteps: Int,
92+
): ConsistSchedule {
93+
val boundaries = consistSchedule.boundaries
94+
val rollingStocks =
95+
consistSchedule.values.map {
96+
parseRawRollingStock(
97+
it.physicsConsist,
98+
it.loadingGaugeType,
99+
it.supportedSignalingSystems,
100+
)
101+
}
102+
return ConsistSchedule(
103+
rollingStocks,
104+
boundaries,
105+
infra,
106+
allowedTrackSections,
107+
totalSteps,
108+
)
109+
}
110+
111+
operator fun invoke(
112+
rollingStocks: List<RollingStock>,
113+
boundaries: List<Int>,
114+
infra: FullInfra,
115+
allowedTrackSections: Set<TrackSectionId>? = null,
116+
totalSteps: Int,
117+
): ConsistSchedule {
118+
// Input validation:
119+
when {
120+
(rollingStocks.size != boundaries.size + 1) -> {
121+
throw OSRDError(ErrorType.InvalidSTDCMInputs)
122+
.withContext(
123+
"cause",
124+
"${boundaries.size} boundaries and ${rollingStocks.size} consist configurations provided. There should be n-1 boundaries for n consist configurations",
125+
)
126+
}
127+
(!boundaries.zipWithNext().all { (a, b) -> a < b }) -> {
128+
throw OSRDError(ErrorType.InvalidSTDCMInputs)
129+
.withContext(
130+
"cause",
131+
"Consist change boundaries are not strictly increasing",
132+
)
133+
}
134+
(!(boundaries.isEmpty() ||
135+
(boundaries.first() != 0 && boundaries.last() != totalSteps - 1))) -> {
136+
throw OSRDError(ErrorType.InvalidSTDCMInputs)
137+
.withContext(
138+
"cause",
139+
"Consist change specified on the first or last step of the path",
140+
)
141+
}
142+
}
143+
144+
// Build the rolling stock and constraint for each step:
145+
val rollingStocksPerStep = mutableListOf<RollingStock>()
146+
val constraints = mutableListOf<PathfindingConstraint>()
147+
var previousBoundary = 0
148+
for ((index, rollingStock) in rollingStocks.withIndex()) {
149+
val boundary = boundaries.getOrNull(index) ?: totalSteps
150+
val constraint =
151+
CachedBlockConstraintCombiner(
152+
initConstraints(infra, rollingStock, allowedTrackSections)
153+
)
154+
(previousBoundary..<boundary).forEach { _ ->
155+
rollingStocksPerStep.add(rollingStock)
156+
constraints.add(constraint)
157+
}
158+
previousBoundary = boundary
159+
}
160+
return ConsistSchedule(rollingStocksPerStep, constraints)
161+
}
162+
}
163+
}
164+
63165
private fun parseRollingResistance(rjsRollingResistance: RJSRollingResistance?): Davis {
64166
if (rjsRollingResistance == null)
65167
throw OSRDError.newMissingRollingStockFieldError("rolling_resistance")

core/src/main/kotlin/fr/sncf/osrd/api/standalone_sim/SimulationRequest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ enum class AllowanceDistribution {
5858
}
5959
}
6060

61-
class PhysicsConsistModel(
61+
data class PhysicsConsistModel(
6262
@Json(name = "effort_curves") val effortCurves: EffortCurve,
6363
@Json(name = "base_power_class") val basePowerClass: String?,
6464
val length: Length<RollingStock>,

0 commit comments

Comments
 (0)