Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: fix: follow scheduled point even if faster than margins #8096

Merged
merged 1 commit into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Khoyo marked this conversation as resolved.
Show resolved Hide resolved
maxEffortExtraTime = 0.0
} else {
standaloneSimLogger.warn("scheduled point doesn't follow standard allowance")
// TODO: raise warning: scheduled point doesn't follow standard allowance
Khoyo marked this conversation as resolved.
Show resolved Hide resolved
}
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
49 changes: 49 additions & 0 deletions tests/tests/test_timetable_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,52 @@ 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
Loading