Skip to content

Commit c17924e

Browse files
committed
core: etcs: use the braking curves in the simulation
Signed-off-by: Eloi Charpentier <[email protected]>
1 parent 6687e44 commit c17924e

File tree

15 files changed

+165
-51
lines changed

15 files changed

+165
-51
lines changed

core/envelope-sim/src/main/kotlin/fr/sncf/osrd/envelope_sim/EnvelopeSimContext.kt

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
package fr.sncf.osrd.envelope_sim
22

33
import com.google.common.collect.RangeMap
4+
import fr.sncf.osrd.sim_infra.api.Path
5+
import fr.sncf.osrd.utils.DistanceRangeSet
6+
import fr.sncf.osrd.utils.units.Offset
47

58
class EnvelopeSimContext(
69
@JvmField val rollingStock: PhysicsRollingStock,
710
@JvmField val path: PhysicsPath,
811
@JvmField val timeStep: Double,
912
@JvmField
10-
val tractiveEffortCurveMap: RangeMap<Double, Array<PhysicsRollingStock.TractiveEffortPoint>>
13+
val tractiveEffortCurveMap: RangeMap<Double, Array<PhysicsRollingStock.TractiveEffortPoint>>,
14+
/** If the train should follows ETCS rules, this contains some extra context */
15+
val etcsContext: ETCSContext? = null,
1116
) {
17+
18+
data class ETCSContext(
19+
/** Braking curves are computing using ETCS rules when they end in these ranges. */
20+
val brakingRanges: DistanceRangeSet,
21+
/**
22+
* List of switch and buffer stops offsets on the path, up to the first switch/buffer stop
23+
* *after* the end of the path (or right at the end).
24+
*/
25+
val dangerPointOffsets: List<Offset<Path>>,
26+
)
27+
1228
/** Creates a context suitable to run simulations on envelopes */
1329
init {
1430
checkNotNull(tractiveEffortCurveMap)

core/envelope-sim/src/main/kotlin/fr/sncf/osrd/envelope_sim/etcs/ETCSBrakingSimulator.kt

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class ETCSBrakingSimulatorImpl(
4848
limitsOfAuthority: Collection<LimitOfAuthority>,
4949
endsOfAuthority: Collection<EndOfAuthority>
5050
): Envelope {
51+
if (limitsOfAuthority.isEmpty() && endsOfAuthority.isEmpty()) return mrsp
5152
TODO("Not yet implemented")
5253
}
5354
}

core/envelope-sim/src/main/kotlin/fr/sncf/osrd/envelope_sim/pipelines/MaxSpeedEnvelope.kt

+115-29
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,32 @@ import fr.sncf.osrd.envelope_sim.EnvelopeProfile
1212
import fr.sncf.osrd.envelope_sim.EnvelopeSimContext
1313
import fr.sncf.osrd.envelope_sim.StopMeta
1414
import fr.sncf.osrd.envelope_sim.TrainPhysicsIntegrator
15+
import fr.sncf.osrd.envelope_sim.etcs.ETCSBrakingSimulator
16+
import fr.sncf.osrd.envelope_sim.etcs.ETCSBrakingSimulatorImpl
17+
import fr.sncf.osrd.envelope_sim.etcs.EndOfAuthority
18+
import fr.sncf.osrd.envelope_sim.etcs.LimitOfAuthority
1519
import fr.sncf.osrd.envelope_sim.overlays.EnvelopeDeceleration
1620
import fr.sncf.osrd.reporting.exceptions.OSRDError
21+
import fr.sncf.osrd.sim_infra.api.Path
22+
import fr.sncf.osrd.utils.units.Offset
23+
import fr.sncf.osrd.utils.units.meters
1724

1825
/**
1926
* Max speed envelope = MRSP + braking curves It is the max speed allowed at any given point,
2027
* ignoring allowances
2128
*/
2229
object MaxSpeedEnvelope {
30+
31+
/**
32+
* Simple data class for easier processing, local to this file. Combines the stop offset with
33+
* the "etcs" flag.
34+
*/
35+
private data class SimStop(
36+
val offset: Double,
37+
val isETCS: Boolean,
38+
val index: Int, // Index in the stop list
39+
)
40+
2341
fun increase(prevPos: Double, prevSpeed: Double, nextPos: Double, nextSpeed: Double): Boolean {
2442
// Works for both accelerations (forwards) and decelerations (backwards)
2543
return prevSpeed < nextSpeed
@@ -29,10 +47,16 @@ object MaxSpeedEnvelope {
2947
* Generate braking curves overlay everywhere the mrsp decrease (increase backwards) with a
3048
* discontinuity
3149
*/
32-
private fun addBrakingCurves(context: EnvelopeSimContext, mrsp: Envelope): Envelope {
33-
val builder = OverlayEnvelopeBuilder.backward(mrsp)
34-
val cursor = EnvelopeCursor.backward(mrsp)
35-
var lastPosition = mrsp.endPos
50+
private fun addBrakingCurves(
51+
etcsSimulator: ETCSBrakingSimulator,
52+
context: EnvelopeSimContext,
53+
mrsp: Envelope
54+
): Envelope {
55+
var envelope = mrsp
56+
envelope = addETCSBrakingCurves(etcsSimulator, context, envelope)
57+
val builder = OverlayEnvelopeBuilder.backward(envelope)
58+
val cursor = EnvelopeCursor.backward(envelope)
59+
var lastPosition = envelope.endPos
3660
while (cursor.findPartTransition(MaxSpeedEnvelope::increase)) {
3761
if (cursor.getPosition() > lastPosition) {
3862
// The next braking curve already covers this point, this braking curve is hidden
@@ -45,7 +69,7 @@ object MaxSpeedEnvelope {
4569
ConstrainedEnvelopePartBuilder(
4670
partBuilder,
4771
SpeedConstraint(0.0, EnvelopePartConstraintType.FLOOR),
48-
EnvelopeConstraint(mrsp, EnvelopePartConstraintType.CEILING)
72+
EnvelopeConstraint(envelope, EnvelopePartConstraintType.CEILING)
4973
)
5074
val startSpeed = cursor.getSpeed()
5175
val startPosition = cursor.getPosition()
@@ -64,51 +88,113 @@ object MaxSpeedEnvelope {
6488
return builder.build()
6589
}
6690

91+
/** Add braking curves following ETCS rules in relevant places */
92+
private fun addETCSBrakingCurves(
93+
etcsSimulator: ETCSBrakingSimulator,
94+
context: EnvelopeSimContext,
95+
envelope: Envelope
96+
): Envelope {
97+
val etcsRanges = context.etcsContext?.brakingRanges ?: return envelope
98+
val cursor = EnvelopeCursor.backward(envelope)
99+
val limitsOfAuthority = mutableListOf<LimitOfAuthority>()
100+
while (cursor.findPartTransition(MaxSpeedEnvelope::increase)) {
101+
val offset = Offset<Path>(cursor.position.meters)
102+
if (etcsRanges.contains(offset.distance)) {
103+
limitsOfAuthority.add(
104+
LimitOfAuthority(
105+
offset,
106+
cursor.speed,
107+
)
108+
)
109+
}
110+
}
111+
return etcsSimulator.addETCSBrakingParts(envelope, limitsOfAuthority, listOf())
112+
}
113+
67114
/** Generate braking curves overlay at every stop position */
68115
private fun addStopBrakingCurves(
116+
etcsSimulator: ETCSBrakingSimulator,
69117
context: EnvelopeSimContext,
70118
stopPositions: DoubleArray,
71119
curveWithDecelerations: Envelope
72120
): Envelope {
73-
var curveWithDecelerations = curveWithDecelerations
74-
for (i in stopPositions.indices) {
75-
var stopPosition = stopPositions[i]
76-
// if the stopPosition is zero, no need to build a deceleration curve
77-
if (stopPosition == 0.0) continue
78-
if (stopPosition > curveWithDecelerations.endPos) {
79-
if (
80-
TrainPhysicsIntegrator.arePositionsEqual(
81-
stopPosition,
82-
curveWithDecelerations.endPos
83-
)
84-
)
85-
stopPosition = curveWithDecelerations.endPos
86-
else
87-
throw OSRDError.newEnvelopeError(i, stopPosition, curveWithDecelerations.endPos)
88-
}
121+
var envelope = curveWithDecelerations
122+
val stops = makeSimStops(context, stopPositions, envelope)
123+
envelope = addETCSStops(etcsSimulator, context, envelope, stops)
124+
for (stop in stops) {
125+
if (stop.isETCS) continue // Already handled
89126
val partBuilder = EnvelopePartBuilder()
90127
partBuilder.setAttr<EnvelopeProfile>(EnvelopeProfile.BRAKING)
91-
partBuilder.setAttr<StopMeta>(StopMeta(i))
128+
partBuilder.setAttr<StopMeta>(StopMeta(stop.index))
92129
val overlayBuilder =
93130
ConstrainedEnvelopePartBuilder(
94131
partBuilder,
95132
SpeedConstraint(0.0, EnvelopePartConstraintType.FLOOR),
96-
EnvelopeConstraint(curveWithDecelerations, EnvelopePartConstraintType.CEILING)
133+
EnvelopeConstraint(envelope, EnvelopePartConstraintType.CEILING)
97134
)
98-
EnvelopeDeceleration.decelerate(context, stopPosition, 0.0, overlayBuilder, -1.0)
135+
EnvelopeDeceleration.decelerate(context, stop.offset, 0.0, overlayBuilder, -1.0)
99136

100-
val builder = OverlayEnvelopeBuilder.backward(curveWithDecelerations)
137+
val builder = OverlayEnvelopeBuilder.backward(envelope)
101138
builder.addPart(partBuilder.build())
102-
curveWithDecelerations = builder.build()
139+
envelope = builder.build()
140+
}
141+
return envelope
142+
}
143+
144+
/** Add braking parts for any ETCS flagged stop. */
145+
private fun addETCSStops(
146+
simulator: ETCSBrakingSimulator,
147+
context: EnvelopeSimContext,
148+
envelope: Envelope,
149+
stops: List<SimStop>
150+
): Envelope {
151+
val endsOfAuthority =
152+
stops
153+
.filter { it.isETCS }
154+
.map { EndOfAuthority(Offset(it.offset.meters), getDangerPoint(context, it)) }
155+
return simulator.addETCSBrakingParts(envelope, listOf(), endsOfAuthority)
156+
}
157+
158+
/**
159+
* Returns the SVL location: next buffer stop or switch, whichever is closest. If there is any.
160+
*/
161+
private fun getDangerPoint(context: EnvelopeSimContext, stop: SimStop): Offset<Path>? {
162+
val etcsContext = context.etcsContext!!
163+
return etcsContext.dangerPointOffsets.firstOrNull { it.distance.meters >= stop.offset }
164+
}
165+
166+
/**
167+
* Converts the raw double offsets into a data class with some metadata. Handles some of the
168+
* input checking (such as invalid offsets).
169+
*/
170+
private fun makeSimStops(
171+
context: EnvelopeSimContext,
172+
stopOffsets: DoubleArray,
173+
envelope: Envelope
174+
): List<SimStop> {
175+
val res = mutableListOf<SimStop>()
176+
for ((i, stopOffset) in stopOffsets.withIndex()) {
177+
if (stopOffset == 0.0) continue
178+
val isETCS = context.etcsContext?.brakingRanges?.contains(stopOffset.meters) ?: false
179+
var offset = stopOffset
180+
if (offset > envelope.endPos) {
181+
if (TrainPhysicsIntegrator.arePositionsEqual(offset, envelope.endPos))
182+
offset = envelope.endPos
183+
else throw OSRDError.newEnvelopeError(i, offset, envelope.endPos)
184+
}
185+
res.add(SimStop(offset, isETCS, i))
103186
}
104-
return curveWithDecelerations
187+
return res
105188
}
106189

107190
/** Generate a max speed envelope given a mrsp */
108191
@JvmStatic
109192
fun from(context: EnvelopeSimContext, stopPositions: DoubleArray, mrsp: Envelope): Envelope {
110-
var maxSpeedEnvelope = addBrakingCurves(context, mrsp)
111-
maxSpeedEnvelope = addStopBrakingCurves(context, stopPositions, maxSpeedEnvelope)
193+
val etcsSimulator =
194+
ETCSBrakingSimulatorImpl(context.path, context.rollingStock, context.timeStep)
195+
var maxSpeedEnvelope = addBrakingCurves(etcsSimulator, context, mrsp)
196+
maxSpeedEnvelope =
197+
addStopBrakingCurves(etcsSimulator, context, stopPositions, maxSpeedEnvelope)
112198
return maxSpeedEnvelope
113199
}
114200
}

core/envelope-sim/src/test/java/fr/sncf/osrd/envelope/EnvelopePartTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ void testGetMechanicalEnergyConsumed() {
7575
var testRollingStock = SimpleRollingStock.STANDARD_TRAIN;
7676
var testEffortCurveMap = SimpleRollingStock.HYPERBOLIC_EFFORT_CURVE_MAP;
7777
var testPath = new FlatPath(length, 0);
78-
var testContext = new EnvelopeSimContext(testRollingStock, testPath, 4, testEffortCurveMap);
78+
var testContext = new EnvelopeSimContext(testRollingStock, testPath, 4, testEffortCurveMap, null);
7979
var allowanceValue = new AllowanceValue.Percentage(10);
8080

8181
var marecoAllowance = AllowanceTests.makeStandardMarecoAllowance(0, length, 1, allowanceValue);

core/envelope-sim/src/test/java/fr/sncf/osrd/envelope_sim/AllowanceRangesTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ public void regressionTestCornerCase() {
324324
var testPath = new FlatPath(10_000, 0);
325325
var stops = new double[] {300};
326326
var testContext = new EnvelopeSimContext(
327-
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
327+
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
328328
var allowance = new LinearAllowance(
329329
0,
330330
testPath.getLength(),
@@ -347,7 +347,7 @@ public void errorBuildupTest(int nRanges, int nStops) {
347347
var testPath = new FlatPath(10_000, 0);
348348
var rangeLength = testPath.getLength() / nRanges;
349349
var testContext = new EnvelopeSimContext(
350-
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
350+
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
351351
var allowanceRanges = new ArrayList<AllowanceRange>();
352352
for (int i = 0; i < nRanges; i++) {
353353
allowanceRanges.add(

core/envelope-sim/src/test/java/fr/sncf/osrd/envelope_sim/AllowanceTests.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public void complexTestBinarySearchContinuity() {
105105
var length = 50_000;
106106
var trainPath = buildNonElectrified(length, new double[] {0, 800, 35_000, length}, new double[] {0, 50, -10});
107107
var testContext = new EnvelopeSimContext(
108-
SimpleRollingStock.STANDARD_TRAIN, trainPath, 2., SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
108+
SimpleRollingStock.STANDARD_TRAIN, trainPath, 2., SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
109109
var stops = new double[] {};
110110
var maxEffortEnvelope = makeSimpleMaxEffortEnvelope(testContext, 44, stops);
111111
var allowanceValue = new AllowanceValue.Percentage(50);
@@ -490,7 +490,7 @@ public void testDifferentSlopes(int slopeProfile) {
490490
var length = 100_000;
491491
var testPath = buildNonElectrified(length, gradePositions, gradeValues);
492492
var testContext = new EnvelopeSimContext(
493-
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
493+
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
494494
var stops = new double[] {50_000, testContext.path.getLength()};
495495
var maxEffortEnvelope = makeComplexMaxEffortEnvelope(testContext, stops);
496496

@@ -528,7 +528,7 @@ public void testMarecoAcceleratingSlopes() {
528528
var testRollingStock = SimpleRollingStock.STANDARD_TRAIN;
529529
var testPath = buildNonElectrified(length, gradePositions.toArray(), gradeValues.toArray());
530530
var testContext = new EnvelopeSimContext(
531-
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
531+
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
532532
var stops = new double[] {50_000, length};
533533
var maxEffortEnvelope = makeComplexMaxEffortEnvelope(testContext, stops);
534534
var allowanceValue = new AllowanceValue.Percentage(10);
@@ -656,7 +656,7 @@ public void testMarecoHighSlopeAtEnd() {
656656
var gradeValues = new double[] {0, 40, 0};
657657
var testPath = buildNonElectrified(length, gradePositions, gradeValues);
658658
var testContext = new EnvelopeSimContext(
659-
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
659+
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
660660
var stops = new double[] {length};
661661
var begin = 3000;
662662
var end = 8000;

core/envelope-sim/src/test/java/fr/sncf/osrd/envelope_sim/EnvelopeMaintainSpeedTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public void suddenSlope() {
2222
0, 40, -40, 0, 50, -50, 0
2323
});
2424
var testRollingStock = SimpleRollingStock.STANDARD_TRAIN;
25-
var context =
26-
new EnvelopeSimContext(testRollingStock, path, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
25+
var context = new EnvelopeSimContext(
26+
testRollingStock, path, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
2727

2828
var flatMRSP = Envelope.make(EnvelopePart.generateTimes(
2929
List.of(EnvelopeProfile.CONSTANT_SPEED), new double[] {0, 10000}, new double[] {44.4, 44.4}));

core/envelope-sim/src/test/java/fr/sncf/osrd/envelope_sim/MarecoDecelerationTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public static void testDecelerationSection(double startOffset, double endOffset)
3131
var testRollingStock = SimpleRollingStock.STANDARD_TRAIN;
3232
var testPath = new FlatPath(100_000, 0);
3333
var context = new EnvelopeSimContext(
34-
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
34+
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
3535
var mrsp = MaxEffortEnvelopeBuilder.makeSimpleMaxEffortEnvelope(context, 1000);
3636
var builder = OverlayEnvelopeBuilder.backward(mrsp);
3737
var partBuilder = new EnvelopePartBuilder();

core/envelope-sim/src/test/java/fr/sncf/osrd/envelope_sim/MaxEffortEnvelopeTest.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public void testFlatNonConstDec() {
4444
var testRollingStock = SimpleRollingStock.MAX_DEC_TRAIN;
4545
var testPath = new FlatPath(10000, 0);
4646
var testContext = new EnvelopeSimContext(
47-
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
47+
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
4848
var stops = new double[] {6000, testPath.getLength()};
4949
var maxEffortEnvelope = makeSimpleMaxEffortEnvelope(testContext, 44.4, stops);
5050
check(maxEffortEnvelope, INCREASING, CONSTANT, DECREASING, INCREASING, CONSTANT, DECREASING);
@@ -76,7 +76,7 @@ public void testSteepNonConstDec() {
7676
var testRollingStock = SimpleRollingStock.MAX_DEC_TRAIN;
7777
var testPath = new FlatPath(10000, 20);
7878
var testContext = new EnvelopeSimContext(
79-
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
79+
testRollingStock, testPath, TIME_STEP, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
8080
var stops = new double[] {6000, testPath.getLength()};
8181
var maxEffortEnvelope = makeSimpleMaxEffortEnvelope(testContext, 44.4, stops);
8282
check(maxEffortEnvelope, INCREASING, DECREASING, INCREASING, DECREASING);
@@ -143,7 +143,7 @@ public void testNotEnoughTractionToStart() {
143143
var length = 10_000;
144144
var path = new FlatPath(length, 1_000);
145145
var testContext = new EnvelopeSimContext(
146-
SimpleRollingStock.STANDARD_TRAIN, path, 2., SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
146+
SimpleRollingStock.STANDARD_TRAIN, path, 2., SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
147147
var stops = new double[] {length};
148148
OSRDError osrdError =
149149
assertThrows(OSRDError.class, () -> makeSimpleMaxEffortEnvelope(testContext, 44.4, stops));
@@ -155,7 +155,7 @@ public void testNotEnoughTractionToRestart() {
155155
var length = 10_000;
156156
var path = buildNonElectrified(length, new double[] {0, 5_000, 5_100, length}, new double[] {0, 1_000, 0});
157157
var testContext = new EnvelopeSimContext(
158-
SimpleRollingStock.STANDARD_TRAIN, path, 2., SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
158+
SimpleRollingStock.STANDARD_TRAIN, path, 2., SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
159159
var stops = new double[] {5_100, length};
160160
OSRDError osrdError =
161161
assertThrows(OSRDError.class, () -> makeSimpleMaxEffortEnvelope(testContext, 44.4, stops));
@@ -192,7 +192,7 @@ public void testSingleStepDeclivity() {
192192
var testRollingStock = SimpleRollingStock.SHORT_TRAIN;
193193
var testPath = new FlatPath(10, 1_000);
194194
var testContext = new EnvelopeSimContext(
195-
testRollingStock, testPath, TIME_STEP * 10, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP);
195+
testRollingStock, testPath, TIME_STEP * 10, SimpleRollingStock.LINEAR_EFFORT_CURVE_MAP, null);
196196
var stops = new double[] {};
197197
var speeds = TreeRangeMap.<Double, Double>create();
198198
speeds.put(Range.open(0., 9.), 40.);

0 commit comments

Comments
 (0)