Skip to content

core: handle consist changes#15992

Open
Sh099078 wants to merge 9 commits intodevfrom
lf/changement-convoi
Open

core: handle consist changes#15992
Sh099078 wants to merge 9 commits intodevfrom
lf/changement-convoi

Conversation

@Sh099078
Copy link
Copy Markdown
Contributor

No description provided.

@github-actions github-actions bot added area:core Work on Core Service area:integration-tests Work on Integration test, by nature related to different services labels Mar 30, 2026
@Sh099078 Sh099078 force-pushed the lf/changement-convoi branch 3 times, most recently from e3597fb to a2f9c6b Compare March 30, 2026 09:50
@Khoyo Khoyo moved this to In Progress in Board PI 19 Mar 30, 2026
@Sh099078 Sh099078 force-pushed the lf/changement-convoi branch 4 times, most recently from d452fdd to a033171 Compare March 31, 2026 09:20
@Khoyo Khoyo requested a review from Erashin March 31, 2026 13:05
@Sh099078 Sh099078 force-pushed the lf/changement-convoi branch 3 times, most recently from b039557 to d9c0ee3 Compare March 31, 2026 15:03
@github-actions github-actions bot removed the area:integration-tests Work on Integration test, by nature related to different services label Mar 31, 2026
Copy link
Copy Markdown
Contributor

@Erashin Erashin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huge PR, thanks for the work.
Was kinda hard to review ngl, this is dense, I'll need to re-review it later on when you've fixed the conflicts/e2e tests/integration tests. Reviewing it with you both in a call would probably help :)
Noting it right now just so we don't forget later on: we'll need to add a description to the PR and modify the commit body/header.

it.copy(
supportedSignalingSystems =
it.supportedSignalingSystems.filter {
// Ignoring ETCS as it is not (yet) supported for
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for STDCM

}
return distanceRangeMapOf(rangeMapEntries)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: A method description to specify this fails if the distance shifts the boundaries to negative values could be nice.

it.copy(
supportedSignalingSystems =
it.supportedSignalingSystems.filter {
// Ignoring ETCS as it is not (yet) supported for
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for STDCM

generateDebugData(infra, path, simulationResponse, departureTime, requirements)
)
}
companion object {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might not understand the intricacies of a companion object, but what's the point of moving those private methods into a companion object exactly? Especially since we don't really use them elsewhere it seems?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of a companion object, but what's the point of moving those private methods into a companion object exactly

They aren't private anymore, and being in the companion object, you can think of them as like static methods in java here. They could have been free functions to tbh.

The main use is they are useful for testing/debugging, but not easily usable when you need to build the whole endpoint. We do have an @disabled test that showcases this.

}

val criticalPoint = end + rollingStock.length
val criticalPoint = end + (rollingStock.length)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think you forgot to remove the parentheses.

endAtStop(),
)
)
val spacingRequirementAutomaton = spacingRequirementAutomaton.clone().updateCallbacks(null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I feel like this should be a bug since behavior changes. I know this method is used in PostProcessingSimulation.findConflictOffsets. Are callbacks not being used at all there? If so, what's actually the point of the method updateCallbacks, since its only use is in this method?

Copy link
Copy Markdown
Contributor

@Khoyo Khoyo Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out, in every use of the callbacks (aka. spacingRequirementAutomaton.processUpdate()) in the infra explorer, we update the callbacks just before. So we don't need to have callbacks ready here.

tbh, the callbacks should probably just be a parameter to processUpdate()

val previousStepPos = 0.meters
return distanceRangeMapOf(
getStepTracker()
.iterateSeenStepsBackwards()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we're iterating backwards on the seen steps only to reverse them further along. We should probably use getSeenSteps directly.

.plus(
run {
val lastStep =
getStepTracker().iterateSeenStepsBackwards().lastOrNull { it.isPlanned }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't the last step since you're using iterateSeenStepsBackwards, this is the first planned step. That might be what you want, I don't quite understand this part of the code, but it doesn't match the name of the variable.

val stepPos = step.travelledPathOffset.distance
DistanceRangeMap.RangeMapEntry(previousStepPos, stepPos, rollingStock)
}
.plus(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need some more context here: what are we actually doing with the plus() here? Or aiming to do?

}

/** Get the index of the current reached step */
fun getCurrentReachedStep(): Int {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so, for your case, I'm guessing you want the reached step excluding overtakes because you want it to determine which rolling stock to use. However, that means the name of the method and its implementation are not on phase. getCurrentReachedStep (which should probably be renamed to getCurrentReachedStepIndex) should just be returning nSeenStepsExcludingLookahead - 1.
If we want to keep the method implementation as is, we should probably rename it to something lik getCurrentReachedPlannedStepIndex (which is long yeah, but does fit the implementation better).

Co-authored-by: Younes Khoudli <younes.khoudli@epita.fr>
Signed-off-by: Loup Federico <16464925+Sh099078@users.noreply.github.com>
@Sh099078 Sh099078 force-pushed the lf/changement-convoi branch from d9c0ee3 to 98a39e6 Compare April 1, 2026 13:46
@github-actions github-actions bot added the area:front Work on Standard OSRD Interface modules label Apr 1, 2026
Copy link
Copy Markdown
Contributor

@eckter eckter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive, good job.

It adds so much complexity though (as expected). I feel like with these changes we need a lot more precise unit tests, and some classes could be split up or refactored.

Hopefully someday we'll have to time to work on #15685 and restructure all this.

Note: I haven't carefully checked all the places where there could be off-by-ones.

val constraint =
CachedBlockConstraintCombiner(
initConstraints(infra, rollingStock, allowedTrackSections)
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using getCachedConstraintCombiner would mean we could share identical constraint caches, across requests but also within one request with consist changes

Comment on lines +70 to +80
/**
* Associates a list of rolling stocks with their related pathfinding constraints. This class
* provides two ways to be built:
* - From a list of STDCM query inputs.
* - From a list of rolling stocks and their pathfinding constraints. This approach is mostly useful
* for testing purposes.
*/
data class ConsistSchedule(
val rollingStocks: List<PhysicsRollingStock>,
val constraints: List<PathfindingConstraint>?,
) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring could explain that it has one entry per path step (even when it doesn't change). I didn't understand that immediately. Especially since we should actually need n-1 consist entries and not n, though I guess it can make the code more straightforward.

The class could have its own file.

location,
steps = steps,
constraints = constraints,
constraints = constraints?.let { MutableList(waypoints.size) { _ -> it } },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For non-STDCM pathfinding, this can result in very long lists. Imported trains can have several hundred waypoints.

I'll keep doing the review with this in mind to see if it would cause issues

.map { it.copy(offset = it.offset - currentOffset, time = it.time - currentTime) }
.filter { it.offset.meters <= envelope.endPos && it.offset.meters > 0 }
val ranges = makeAllowanceRanges(envelope, shiftedFixedPoints)
if (ranges.isEmpty()) finalEnvelopes.add(envelope)
Copy link
Copy Markdown
Contributor

@eckter eckter Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition should also skip the rest of the loop.

Which also means we lack unit tests here, this part of the code is really hard to debug on real-life scenarios, when the bugs aren't caught by precise unit tests.

(Yeah we already lack tests there, it would just be worse with the added complexity)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I wrote a test where we have no fixed point, hoping it would trigger the bug... And it didn't.

Looking at makeAllowanceRanges():

/** Create the list of `AllowanceRange`, with the given fixed points */
private fun makeAllowanceRanges(
    envelope: Envelope,
    fixedPoints: Collection<FixedTimePoint>,
): List<AllowanceRange> {
    var transition = 0.0
    var transitionTime = 0.0
    var prevAddedTime = 0.0
    val res = ArrayList<AllowanceRange>()
    for (point in fixedPoints) {
        val baseTime =
            envelope.interpolateArrivalAtClamp(point.offset.meters) -
                envelope.interpolateDepartureFromClamp(transition)
        val pointArrivalTime = transitionTime + baseTime
        val neededDelay = max(0.0, point.time - pointArrivalTime - prevAddedTime)

        res.add(
            AllowanceRange(transition, point.offset.meters, AllowanceValue.FixedTime(neededDelay))
        )
        prevAddedTime += neededDelay

        transitionTime += baseTime + (point.stopTime ?: 0.0)
        transition = point.offset.meters
    }
    if (transition < envelope.endPos)
        res.add(AllowanceRange(transition, envelope.endPos, AllowanceValue.FixedTime(0.0)))

    return res
}

Either we enter the loop, and we add a range. Or we don't, but then transition == 0.0 and we add a range unless we are dealing with a zero-sized envelope.

Trying to add a zero-sized enveloppe to the test, this breaks again... Because we try to create zero-sized SubPhysicsPath.

Anyways, I now believe the proper fix is to just assert that we do have at least one AllowanceRange, even if it's one where we don't loose any time.

Fix and test here: a1d2f45

@@ -428,29 +444,55 @@ private fun getUpdatedExplorer(
* set to true, the allowances follow the mareco distribution (more accurate but less reliable).
*/
private fun runSimulationWithFixedPoints(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function could maybe have been simpler if we kept the old one and wrapped it in a loop

override fun getFullRollingStockRangeMap(): DistanceRangeMap<PhysicsRollingStock> {
val cache = rollingStockRangeMapCache?.get()
if (cache != null) return cache
val previousStepPos = 0.meters
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that this is read-only feels very sus

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops. Fixed in 590b93a

@Sh099078 Sh099078 force-pushed the lf/changement-convoi branch from 98a39e6 to 9adf44b Compare April 1, 2026 16:00
@github-actions github-actions bot removed the area:front Work on Standard OSRD Interface modules label Apr 1, 2026
@Sh099078 Sh099078 force-pushed the lf/changement-convoi branch from ab27251 to 3210643 Compare April 1, 2026 16:15
@Sh099078 Sh099078 marked this pull request as ready for review April 1, 2026 16:28
@Sh099078 Sh099078 requested a review from a team as a code owner April 1, 2026 16:28
@Sh099078 Sh099078 requested a review from Khoyo April 1, 2026 16:28
Signed-off-by: Younes Khoudli <younes.khoudli@epita.fr>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Work on Core Service

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

4 participants