Skip to content

Commit

Permalink
core: fix: follow scheduled point even if faster than margins
Browse files Browse the repository at this point in the history
  • Loading branch information
eckter committed Jul 17, 2024
1 parent 5676323 commit e270638
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ fun makeMRSPResponse(speedLimits: Envelope): MRSPResponse {

/**
* Build the final envelope from the max effort / provisional envelopes. The final envelope modifies
* the margin ranges to match the scheduled points.
* the margin ranges to match the scheduled points. The added time is distributed over the different
* margin ranges, following a logic described in details on the OSRD website:
* https://osrd.fr/en/docs/reference/design-docs/timetable/#combining-margins-and-schedule
*/
fun buildFinalEnvelope(
maxEffortEnvelope: Envelope,
Expand All @@ -204,6 +206,9 @@ fun buildFinalEnvelope(
fun getEnvelopeTimeAt(offset: Offset<TravelledPath>): Double {
return provisionalEnvelope.interpolateDepartureFromClamp(offset.distance.meters)
}
fun getMaxEffortEnvelopeTimeAt(offset: Offset<TravelledPath>): Double {
return maxEffortEnvelope.interpolateDepartureFromClamp(offset.distance.meters)
}
var prevFixedPointOffset = Offset<TravelledPath>(0.meters)
var prevFixedPointDepartureTime = 0.0
val marginRanges = mutableListOf<AllowanceRange>()
Expand All @@ -217,24 +222,51 @@ fun buildFinalEnvelope(
val sectionTime =
getEnvelopeTimeAt(point.pathOffset) - getEnvelopeTimeAt(prevFixedPointOffset)
val arrivalTime = prevFixedPointDepartureTime + sectionTime
var extraTime = point.arrival.seconds - arrivalTime
if (extraTime < 0.0) {
// TODO: raise a warning
standaloneSimLogger.warn("impossible scheduled point")
extraTime = 0.0
}
marginRanges.addAll(
distributeAllowance(
maxEffortEnvelope,
provisionalEnvelope,
extraTime,
margins,
prevFixedPointOffset,
point.pathOffset
val extraTime = point.arrival.seconds - arrivalTime
if (extraTime >= 0.0) {
marginRanges.addAll(
distributeAllowance(
maxEffortEnvelope,
provisionalEnvelope,
extraTime,
margins,
prevFixedPointOffset,
point.pathOffset
)
)
)
prevFixedPointDepartureTime = arrivalTime + extraTime + (point.stopFor?.seconds ?: 0.0)
} else {
// We need to *remove* time compared to the provisional envelope.
// Ideally we would distribute the (negative) extra time following the same logic as
// when it's positive. But this is tricky: as we get closer to max effort envelope (hard
// limit), we need to redistribute the time in some cases.
// We currently handle this by ignoring the distribution over different margin ranges,
// we just set the time for the scheduled point without more details. It will be easier
// to handle it properly when we'll have migrated to standalone sim v3.
val maxEffortSectionTime =
getMaxEffortEnvelopeTimeAt(point.pathOffset) -
getMaxEffortEnvelopeTimeAt(prevFixedPointOffset)
val earliestPossibleArrival = prevFixedPointDepartureTime + maxEffortSectionTime
var maxEffortExtraTime = point.arrival.seconds - earliestPossibleArrival
if (maxEffortExtraTime < 0.0) {
standaloneSimLogger.warn("impossible scheduled point")
// TODO: raise warning: scheduled point isn't possible
maxEffortExtraTime = 0.0
} else {
standaloneSimLogger.warn("scheduled point doesn't follow standard allowance")
// TODO: raise warning: scheduled point doesn't follow standard allowance
}
marginRanges.add(
AllowanceRange(
prevFixedPointOffset.distance.meters,
point.pathOffset.distance.meters,
FixedTime(maxEffortExtraTime)
)
)
prevFixedPointDepartureTime =
earliestPossibleArrival + maxEffortExtraTime + (point.stopFor?.seconds ?: 0.0)
}
prevFixedPointOffset = point.pathOffset
prevFixedPointDepartureTime = arrivalTime + extraTime + (point.stopFor?.seconds ?: 0.0)
}
val pathEnd = Offset<TravelledPath>(maxEffortEnvelope.endPos.meters)
if (prevFixedPointOffset < pathEnd) {
Expand Down
52 changes: 52 additions & 0 deletions tests/tests/test_timetable_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,55 @@ def test_conflicts(
assert response.status_code == 200
actual_conflicts = {conflict["conflict_type"] for conflict in response.json()}
assert actual_conflicts == expected_conflict_types


def test_scheduled_points_with_incompatible_margins(
small_infra: Infra,
timetable_v2: TimetableV2,
fast_rolling_stock: int,
):
requests.post(f"{EDITOAST_URL}infra/{small_infra.id}/load").raise_for_status()
train_schedule_payload = [
{
"comfort": "STANDARD",
"constraint_distribution": "STANDARD",
"initial_speed": 0,
"labels": [],
"options": {"use_electrical_profiles": False},
"path": [
{"id": "start", "track": "TC0", "offset": 185000},
{"id": "end", "track": "TD0", "offset": 24820000},
],
"power_restrictions": [],
"rolling_stock_name": "fast_rolling_stock",
"schedule": [
{
"at": "start",
},
{
"at": "end",
"arrival": "PT4000S",
},
],
"margins": {
"boundaries": [],
"values": ["100%"]
},
"speed_limit_tag": "MA100",
"start_time": "2024-05-22T08:00:00.000Z",
"train_name": "name",
}
]
response = requests.post(
f"{EDITOAST_URL}/v2/timetable/{timetable_v2.id}/train_schedule", json=train_schedule_payload
)
response.raise_for_status()
train_id = response.json()[0]["id"]
response = requests.get(f"{EDITOAST_URL}/v2/train_schedule/{train_id}/simulation/?infra_id={small_infra.id}")
response.raise_for_status()
content = response.json()
sim_output = content["final_output"]
travel_time_seconds = sim_output["times"][-1] / 1_000

# Should arrive roughly 4000s after departure, even if that doesn't fit the margins
assert abs(travel_time_seconds - 4_000) < 2

0 comments on commit e270638

Please sign in to comment.