From cafbd21cc201adb367723d8377cd8cd36d6c72da Mon Sep 17 00:00:00 2001 From: Erashin Date: Mon, 3 Mar 2025 15:50:20 +0100 Subject: [PATCH 1/2] core: compute min between 2 envelope parts Signed-off-by: Erashin --- core/envelope-sim/build.gradle | 3 + .../sncf/osrd/envelope/part/EnvelopePart.java | 64 +++++++++++++++++-- .../sncf/osrd/envelope/EnvelopePartTest.java | 48 +++++++++++++- 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/core/envelope-sim/build.gradle b/core/envelope-sim/build.gradle index 5a7422c00d4..688a136e67b 100644 --- a/core/envelope-sim/build.gradle +++ b/core/envelope-sim/build.gradle @@ -41,6 +41,9 @@ dependencies { // Use JUnit Jupiter Engine for testing. testRuntimeOnly libs.junit.jupiter.engine + // Use AssertJ for testing + testImplementation libs.assertj + // for linter annotations testFixturesCompileOnly libs.jcip.annotations testFixturesCompileOnly libs.spotbugs.annotations diff --git a/core/envelope-sim/src/main/java/fr/sncf/osrd/envelope/part/EnvelopePart.java b/core/envelope-sim/src/main/java/fr/sncf/osrd/envelope/part/EnvelopePart.java index a5639c8c8b5..8f411f7e5b6 100644 --- a/core/envelope-sim/src/main/java/fr/sncf/osrd/envelope/part/EnvelopePart.java +++ b/core/envelope-sim/src/main/java/fr/sncf/osrd/envelope/part/EnvelopePart.java @@ -9,10 +9,7 @@ import fr.sncf.osrd.envelope_sim.EnvelopeProfile; import fr.sncf.osrd.envelope_utils.ExcludeFromGeneratedCodeCoverage; import fr.sncf.osrd.utils.SelfTypeHolder; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; /** @@ -616,6 +613,65 @@ public EnvelopePart copyAndShift(double positionDelta, double minPosition, doubl new HashMap<>(attrs), newPositions.toArray(), newSpeeds.toArray(), newTimeDeltas.toArray()); } + /** + * Compute the minimum EnvelopePart between 2 envelope parts with an intersecting range [a, b]. + * The resulting envelope part starts at the envelope with the minimum speed at a and ends at b. + */ + public static EnvelopePart min( + EnvelopePart envelopePartA, EnvelopePart envelopePartB, Iterable attrs) { + var beginPosA = envelopePartA.getBeginPos(); + var beginPosB = envelopePartB.getBeginPos(); + var endPosA = envelopePartA.getEndPos(); + var endPosB = envelopePartB.getEndPos(); + assert (beginPosA < endPosB && endPosA > beginPosB); + var startIntersectingRange = Math.max(beginPosA, beginPosB); + var start = envelopePartA.interpolateSpeed(startIntersectingRange) + <= envelopePartB.interpolateSpeed(startIntersectingRange) + ? beginPosA + : beginPosB; + var end = Math.min(endPosA, endPosB); + + TreeSet keyPositions = + Arrays.stream(envelopePartA.positions).boxed().collect(Collectors.toCollection(TreeSet::new)); + keyPositions.addAll(Arrays.stream(envelopePartB.positions).boxed().collect(Collectors.toSet())); + keyPositions = new TreeSet<>(keyPositions.subSet(start, true, end, true)); + var keyPosList = new ArrayList<>(keyPositions); + + var newPositions = new DoubleArrayList(); + var newSpeeds = new DoubleArrayList(); + for (int i = 0; i < keyPosList.size(); i++) { + var pos = keyPosList.get(i); + boolean inEnvelopePartA = pos >= beginPosA; + boolean inEnvelopePartB = pos >= beginPosB; + + double speedA = inEnvelopePartA ? envelopePartA.interpolateSpeed(pos) : Double.POSITIVE_INFINITY; + double speedB = inEnvelopePartB ? envelopePartB.interpolateSpeed(pos) : Double.POSITIVE_INFINITY; + double minSpeedAtPos = Math.min(speedA, speedB); + + if (i > 0) { + double prevPos = keyPosList.get(i - 1); + boolean prevInEnvelopePartA = prevPos >= beginPosA; + boolean prevInEnvelopePartB = prevPos >= beginPosB; + + if (inEnvelopePartA && inEnvelopePartB && prevInEnvelopePartA && prevInEnvelopePartB) { + double prevSpeedA = envelopePartA.interpolateSpeed(prevPos); + double prevSpeedB = envelopePartB.interpolateSpeed(prevPos); + + if ((prevSpeedA - prevSpeedB) * (speedA - speedB) < 0) { + var intersection = EnvelopePhysics.intersectSteps( + prevPos, prevSpeedA, pos, speedA, prevPos, prevSpeedB, pos, speedB); + // Add intersection point + newPositions.add(intersection.position); + newSpeeds.add(intersection.speed); + } + } + } + newPositions.add(pos); + newSpeeds.add(minSpeedAtPos); + } + return generateTimes(attrs, newPositions.toArray(), newSpeeds.toArray()); + } + // endregion // region EQUALS diff --git a/core/envelope-sim/src/test/java/fr/sncf/osrd/envelope/EnvelopePartTest.java b/core/envelope-sim/src/test/java/fr/sncf/osrd/envelope/EnvelopePartTest.java index 14c157a5741..07ccd342407 100644 --- a/core/envelope-sim/src/test/java/fr/sncf/osrd/envelope/EnvelopePartTest.java +++ b/core/envelope-sim/src/test/java/fr/sncf/osrd/envelope/EnvelopePartTest.java @@ -1,14 +1,19 @@ package fr.sncf.osrd.envelope; import static fr.sncf.osrd.envelope.EnvelopePhysics.getPartMechanicalEnergyConsumed; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import fr.sncf.osrd.envelope.part.EnvelopePart; import fr.sncf.osrd.envelope_sim.*; import fr.sncf.osrd.envelope_sim.allowances.utils.AllowanceValue; +import java.util.Collections; import java.util.List; -import org.junit.jupiter.api.Assertions; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class EnvelopePartTest { @Test @@ -93,7 +98,7 @@ void testGetMechanicalEnergyConsumed() { / 1_000_000; break; case 1: - Assertions.assertEquals(envelopePart.getMinSpeed(), envelopePart.getMaxSpeed()); + assertEquals(envelopePart.getMinSpeed(), envelopePart.getMaxSpeed()); expectedEnvelopePartEnergy = testRollingStock.getRollingResistance(envelopePart.getBeginSpeed()) * envelopePart.getTotalDistance(); break; @@ -109,4 +114,43 @@ void testGetMechanicalEnergyConsumed() { assertEquals(expectedEnvelopePartEnergy, envelopePartEnergy, 0.1 * expectedEnvelopePartEnergy + 1000); } } + + @ParameterizedTest + @MethodSource("minEnvelopePartsArgs") + void testMinEnvelopeParts( + EnvelopePart envelopePartA, + EnvelopePart envelopePartB, + Stream intersectionIndexes, + double[] expectedPositions, + double[] expectedSpeeds) { + var minEnvelope = EnvelopePart.min( + envelopePartA, envelopePartB, Collections.singleton(envelopePartA.getAttr(EnvelopeProfile.class))); + var resultingPositions = minEnvelope.clonePositions(); + var resultingSpeeds = minEnvelope.cloneSpeeds(); + + intersectionIndexes.forEach(intersectionIndex -> { + var intersectionPosition = resultingPositions[intersectionIndex]; + var intersectionSpeed = resultingSpeeds[intersectionIndex]; + assertEquals(envelopePartA.interpolateSpeed(intersectionPosition), intersectionSpeed); + assertEquals(envelopePartB.interpolateSpeed(intersectionPosition), intersectionSpeed); + }); + assertThat(expectedPositions).isEqualTo(resultingPositions); + assertThat(expectedSpeeds).isEqualTo(resultingSpeeds); + } + + static Stream minEnvelopePartsArgs() { + return Stream.of( + Arguments.of( + EnvelopeTestUtils.generateTimes(new double[] {0, 10, 20}, new double[] {100, 50, 0}), + EnvelopeTestUtils.generateTimes(new double[] {5, 14, 16}, new double[] {150, 50, 0}), + Stream.of(4), + new double[] {0, 5, 10, 14, 15, 16}, + new double[] {100, 79.05694150420949, 50, 38.72983346207417, 35.35533905932738, 0}), + Arguments.of( + EnvelopeTestUtils.generateTimes(new double[] {5, 14, 16}, new double[] {100, 50, 0}), + EnvelopeTestUtils.generateTimes(new double[] {0, 10, 20}, new double[] {150, 50, 0}), + Stream.of(1, 4), + new double[] {5, 7.142857142857143, 10, 14, 15, 16}, + new double[] {100, 90.63269671749657, 50, 38.72983346207417, 35.35533905932738, 0})); + } } From 69253bffcab67daf3340464905069e89707a8cf9 Mon Sep 17 00:00:00 2001 From: Erashin Date: Tue, 4 Mar 2025 10:16:56 +0100 Subject: [PATCH 2/2] core: use envelope part min in ETCS Signed-off-by: Erashin --- .../envelope_sim/etcs/ETCSBrakingCurves.kt | 42 ++----------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/core/envelope-sim/src/main/kotlin/fr/sncf/osrd/envelope_sim/etcs/ETCSBrakingCurves.kt b/core/envelope-sim/src/main/kotlin/fr/sncf/osrd/envelope_sim/etcs/ETCSBrakingCurves.kt index 30dc52882b4..9a9b716a2fe 100644 --- a/core/envelope-sim/src/main/kotlin/fr/sncf/osrd/envelope_sim/etcs/ETCSBrakingCurves.kt +++ b/core/envelope-sim/src/main/kotlin/fr/sncf/osrd/envelope_sim/etcs/ETCSBrakingCurves.kt @@ -126,45 +126,11 @@ private fun computeMinSvlEoaIndCurve( releaseSpeedPositionSvl, NATIONAL_RELEASE_SPEED ) - val startIntersectingRange = - max(slicedIndicationCurveEoa.beginPos, slicedIndicationCurveSvl.beginPos) - var refCurve = slicedIndicationCurveEoa - var otherCurve = slicedIndicationCurveSvl - if ( - slicedIndicationCurveEoa.interpolateSpeed(startIntersectingRange) > - slicedIndicationCurveSvl.interpolateSpeed(startIntersectingRange) - ) { - refCurve = slicedIndicationCurveSvl - otherCurve = slicedIndicationCurveEoa - } - val pointCount = refCurve.pointCount() - var indicationPositions = DoubleArray(pointCount) - var indicationSpeeds = DoubleArray(pointCount) - for (i in 0 until pointCount) { - val newPos = refCurve.getPointPos(i) - val newSpeed = refCurve.getPointSpeed(i) - if (newPos < otherCurve.beginPos) { - indicationPositions[i] = newPos - indicationSpeeds[i] = newSpeed - } else if (newPos <= otherCurve.endPos) { - val otherSpeed = slicedIndicationCurveSvl.interpolateSpeed(newPos) - indicationPositions[i] = newPos - // TODO: unneeded for now: interpolate to not approximate position at intersection. - indicationSpeeds[i] = min(otherSpeed, newSpeed) - } else { - indicationPositions[i] = otherCurve.endPos - indicationSpeeds[i] = otherCurve.endSpeed - // Clean up the last unneeded points in the arrays before exiting the loop. - indicationPositions = indicationPositions.dropLast(pointCount - 1 - i).toDoubleArray() - indicationSpeeds = indicationSpeeds.dropLast(pointCount - 1 - i).toDoubleArray() - break - } - } val firstIndicationPart = - EnvelopePart.generateTimes( - listOf(EnvelopeProfile.BRAKING), - indicationPositions, - indicationSpeeds + EnvelopePart.min( + slicedIndicationCurveEoa, + slicedIndicationCurveSvl, + listOf(EnvelopeProfile.BRAKING) ) if (releaseSpeedPositionSvl < releaseSpeedPositionEoa) { val maintainReleaseSpeedPart =