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
Interpolate increasing stop times for GTFS-RT cancelled trips #5348
Interpolate increasing stop times for GTFS-RT cancelled trips #5348
Conversation
…mes. Added test cases for stop cancellations and no_data cases.
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## dev-2.x #5348 +/- ##
=============================================
+ Coverage 66.85% 66.87% +0.02%
- Complexity 15651 15661 +10
=============================================
Files 1817 1817
Lines 70158 70201 +43
Branches 7379 7385 +6
=============================================
+ Hits 46903 46946 +43
- Misses 20798 20799 +1
+ Partials 2457 2456 -1
☔ View full report in Codecov by Sentry. |
I think we don't enforce that NO_DATA cannot have arrival/departure time and the spec says that you should not include times for those stop times. This means that in the OTP model those stop times do have a time, are routable and that can indeed lead to time traveling situations. |
src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java
Outdated
Show resolved
Hide resolved
I added the NO_DATA case because locally we use NO_DATA when the onboard computer fails to select the correct trip or when the driver forgets to log in. In this case, there is no real-time data applicable. Should I remove the NO_DATA case? Or maybe we can also add a field in the API results to indicate if the stop is cancelled or has no real-time data? |
Yes, please remove the no_data case. No_data to OTP means that there is no realtime data available but the stop still has a scheduled time which we will display to the user and use in routing. For that reason it must be increasing. Each stop time already has a realtime state field, which contains this exact information. Which API do you use? https://docs.opentripplanner.org/en/dev-2.x/Apis/ |
…Times.java Co-authored-by: Thomas Gran <t2gran@gmail.com>
You need to run |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me.
@lassetyr is responsible for the Siri realtime updater and he will check if this is has any effect there.
It looks like this is based on the assumption that a stop has either
We have seen some corner-cases in SIRI realtime where a cancelled stop may have invalid/negative dwell-time - i.e. the realtime data is present, but it may be wrong/negative. When this happens, the trip-search breaks with this change. Described by adjusting one of the tests:
The negative dwell-time on stop 6 is ignored since it is cancelled (as expected), so the trip now describes a trip back in time between stop 5 and 6. Finally, when doing a trip-search from 5 to 6, I get the result: |
But how can you travel from 5 to 6 when 6 is cancelled? I thought that cancelled stops are excluded from RAPTOR. |
Yeah, these assumptions are from GTFS specs and the current GTFS update implementations from OTP, because I am not familiar with the SIRI updates. So, I manually created a SIRI update for one trip with some local data. However, I couldn't replicate the issue you mentioned. In the example, stops 18 to 32 are cancelled, I set the times between 18 and 31 to 00:00:00 which has negative hop times from stop 17, and 32 has a negative dwell time while being cancelled. If I plan a trip from stop 17 to stop 32, OTP shows me to alight at stop 33 and walk back. The SIRI update:
The legs of my itinerary in JSON
|
This is the result we want, isn't it? All the stops in between are cancelled so walking back is the only option. |
I did a trip-search from 5 to 6 - Note: I also had set Trip-query: https://gist.github.com/lassetyr/02eca07c0e70d650a5bda7fb3cd3b0e9 Stops/calls in a complete SIRI-update - as usage has been defined in EU - always has both arrival- and departureTimes - the complete SIRI-update that resulted in the error can be found at: https://gist.github.com/lassetyr/d9890372b03291706491de62130983dd Stop 1: 20 minutes delay (Sending these negative values is not allowed, but we still see that it happens) Full NeTEx-dataset loaded: https://storage.googleapis.com/marduk-production/outbound/netex/rb_norway-aggregated-netex.zip I now also tested sending different versions of the SIRI-update with valid/invalid times. This causes the error to eventually go away - even if invalid data is sent. With a full restart of OTP and reapplying the invalid data, it reappears. I have not looked into why this happens... Is there a unit/integration test that tests trip-searches with applied realtime-data? |
@wangyuxuan-symun As you're aware we debated this and while we haven't come to a firm conclusion we have a couple of ideas. One of them would be to propagate the delay to the cancelled stops, however there were also concerns about simply inventing a time. We will be thinking about this a little more and hope to come back to this problem. |
Sorry that I had to leave early... Personally, I prefer to either apply the entire update or ignore the entire update. Otherwise, the time variable would have two meanings: scheduled and real-time at the same time, which is confusing and conflicting. For canceled stops, we drop the times from our electronic display boards. The apps would show the scheduled time but greyed out with a line crossing it out so passengers know which trip is affected. I don't like the idea of inventing a time. If we really want to invent a time, I think delays have to be interpolated between the non-cancelled stop before and the non-cancelled stop after. We have a lot of stop cancellations where the buses take the highway to skip the regular route, which will catch up with the delays and end up running early. Thus, if we apply the same delay to the canceled stops, we will still end up with non-increasing times... FYI, I will be on vacation for the next few weeks, so my response time will be slower than usual. |
We had a quick discussion about this at the last dev meeting. Since we have the requirement of Siri trips that have to be rejected when the time on their cancelled stops are not increasing we are left with only one choice: propagate the delay forward to the cancelled stops so that they, too, become increasing. @optionsome is the expert for the delay propagation. Could you point out where this happens? |
For forwards direction (this is what the specification says should happen) https://github.com/opentripplanner/OpenTripPlanner/blob/dev-2.x/src/main/java/org/opentripplanner/model/Timetable.java#L311-L312 and backwards (this is something we are forced to do to ensure the timetable is usable in routing) https://github.com/opentripplanner/OpenTripPlanner/blob/dev-2.x/src/main/java/org/opentripplanner/model/Timetable.java#L323-L344 |
Just so I get this right: it is also supposed to work in the absence of the |
* Interpolate the times for CANCELLED stops in between regular stops to ensure internal time | ||
* representations are increasing for these stops. This will not interpolate terminal | ||
* cancellations, as those can be handled by backward and forward propagations. Note: This is | ||
* currently for GTFS-RT only, since SIRI requires times for all stops. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Interpolate the times for CANCELLED stops in between regular stops to ensure internal time | |
* representations are increasing for these stops. This will not interpolate terminal | |
* cancellations, as those can be handled by backward and forward propagations. Note: This is | |
* currently for GTFS-RT only, since SIRI requires times for all stops. | |
* Interpolate the times for CANCELLED stops in between regular stops to ensure internal time | |
* representations are increasing for these stops. This will not interpolate terminal | |
* cancellations, as those can be handled by backward and forward propagations. Note: This is | |
* currently for GTFS-RT only, since SIRI requires times for all stops and there we _want_ to reject trips | |
* where the times for cancelled stops are missing or are non-increasing. |
var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); | ||
TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); | ||
tripUpdateBuilder.setTrip(tripDescriptorBuilder); | ||
|
||
// Stop 1 | ||
StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); | ||
stopTimeUpdateBuilder.setStopSequence(1); | ||
stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); | ||
StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); | ||
stopTimeEventBuilder.setDelay(0); | ||
stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); | ||
stopTimeEventBuilder.setDelay(0); | ||
|
||
// Stop 2 | ||
stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(1); | ||
stopTimeUpdateBuilder.setStopSequence(2); | ||
stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SKIPPED); | ||
|
||
// Stop 3 | ||
stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(2); | ||
stopTimeUpdateBuilder.setStopSequence(3); | ||
stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); | ||
stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); | ||
stopTimeEventBuilder.setDelay(-800); | ||
stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); | ||
stopTimeEventBuilder.setDelay(-800); | ||
|
||
TripUpdate tripUpdate = tripUpdateBuilder.build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can reduce the duplication in this code a lot by using the TripUpdateBuilder
like this:
OpenTripPlanner/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java
Lines 507 to 519 in b5d1a9c
var builder = new TripUpdateBuilder( | |
scheduledTripId, | |
SERVICE_DATE, | |
SCHEDULED, | |
transitModel.getTimeZone() | |
) | |
.addDelayedStopTime(1, 0) | |
.addDelayedStopTime(2, 60, 80) | |
.addDelayedStopTime(3, 90, 90); | |
var tripUpdate = builder.build(); | |
var updater = defaultUpdater(); |
If there are methods missing in that class you can add them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic from of the GTFS side is good but I have left a few comments.
Consider configuring this feature per updater. This should be default off. If this is done, then no explicit exclude for SIRI is needed. Only GTFS updeters need the flag to turn it on. |
No conflict to resolve.
Hello! Sorry for the late update, I was sick for the past two weeks... Based on the earlier discussion, I moved the interpolation method to the Timetable class, which is the GTFS-RT updater. Test cases are moved as well. I also set the method to private, so no explicit exclusion is needed in the documentation. However, I didn't add a new flag to turn it on or off, because the current GTFS updater ignores SKIPPED stop times even if they have times. If we decide to create a generic updater layer later as mentioned in the TODO section, I think maybe we can add the flag at that time. Also I have a question for @leonardehrenfried about the |
assertTrue(patch.isSuccess()); | ||
|
||
patch.ifSuccess(p -> { | ||
var updatedTripTimes = p.getTripTimes(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please run the validateNonIncreasingTimes
in the test to make absolutely sure that they are increasing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validateNonIncreasingTimes
is called at the end of createUpdatedTripTimes
method, and the interpolation is done before validating times. If the times are not increasing, the assertTrue
you referenced here will fail.
// Interpolate missing times from CANCELLED stops. Note: Currently for GTFS-RT ONLY.
if (interpolateMissingTimes(newTimes)) {
LOG.debug("Interpolated delays for cancelled stops on trip {}.", tripId);
}
// Validate for non-increasing times. Log error if present.
var error = newTimes.validateNonIncreasingTimes();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. that's ok then.
That's an oversight. If I replace NO_VALUE with NO_DELAY all the tests still pass. You can modify this method and delete yours. |
When we talked about this in the meeting, @optionsome and @t2gran felt that it was against the GTFS-RT spec to propagate delays on skipped stop times, that's why they requested this flag. I personally can't find anything in the spec that states this. @optionsome, you're usually quite good about this - can you show us the language in the spec that talks about this topic? |
I re-read the specification and it doesn't seem to explicitly touch this subject. The closest thing I could find was "Note that updates with a schedule relationship of SKIPPED will not stop delay propagation" in https://gtfs.org/realtime/feed-entities/trip-updates/ . This is sort of a weird topic for most contexts since we the vehicle doesn't actually visit the skipped stop so having any estimate for it doesn't really make sense. However, propagating the delay to skipped stops can lead to the departure time of a skipped stop being after the arrival time to the next stop if there is an estimate for the next stop. This can lead to issues if we do routing the include cancellation flag on. Although, the similar issues can exist if we don't propagate the delay. I'm really not sure if we are doing something according or against the specification if we propagate. |
/** | ||
* Add a skipped stop to the TripUpdate. | ||
*/ | ||
public TripUpdateBuilder addSkippedStop(int stopSequence) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having this method is also fine so no need to remove it if you don't want to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about I add another method for NO_DATA cases which also don't have times associated, and remove the addStopTime
method I mentioned earlier, so that the StopTimes can be built more explicitly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's a good idea.
Here's the paragraph from the documentation in https://gtfs.org/realtime/feed-entities/trip-updates/.
I think there are cases where propagation is required, and sometimes propagation is not required. Here's my understanding. Say we have 3 stops, scheduled at 0, 60, 120. Case 1: If all stops are regular stops, and the trip updates only contain information for stop 1, but not for stop 2 and 3. Hence, in "the absence of any other information", and we don't have "any other information", stop 2 and 3 will get the same delay from stop 1. I think propagation here is required, and this is what OTP does. For example, if the delay for stop 1 is 200 (arrive at 200), stop 2 and 3 will get delayed by 100 (arrive at 260, 320). Case 2: If stop 1 and 3 are regular stops, stop 2 is cancelled, and the trip updates only contain information for stop 1 and 2, but not for stop 3.From the documentation "updates with a schedule relationship of SKIPPED will not stop delay propagation". I think propagation here is required, we can propagate delays after stop 2, and this is what OTP does. For example, if the delay for stop 1 is 200 (arrive at 200), stop 2 is cancelled (OTP internal representation 260), then 3 will get a propagated delay of 200 (arrive at 320) in the "absent of any other information" for stop 3. Case 3: If stop 1 and 3 are regular stops, stop 2 has no data, and the trip updates only contain information for stop 1 and 2, but not for stop 3. From the documentation "updates with schedule relationships of SCHEDULED or NO_DATA will" stop propagation. So, propagation is not allowed, we cannot propagate delays after stop 2 absent of any information. This is what OTP does. Example, if the delay for stop 1 is 200 (arrive at 200), stop 2 has no data (OTP defaults to schedule 60), then 3 will get no propagated delay (defaults to schedule 120). So in this case, we will have non-increasing times in the "absent of any other information" for stop 3. Case 4 (my original issue): If stop 1 and 3 are regular stops, stop 2 is cancelled, and the trip updates contain information for stop 1, 2, and 3. For example, if the delay for stop 1 is 200 (arrive at 200), stop 2 is cancelled (if we propagate delays, arrive at 260), and stop 3 has a delay of 100 (arrive at 220). I agree with @optionsome that in this case
However, the difference in this case is that we have the "other information" that is absent in previous cases, that's why I think it is fine to not propagate the delays and do something else. |
* | ||
* @return true if there is interpolated times, false if there is no interpolation. | ||
*/ | ||
private boolean interpolateMissingTimes(TripTimes newTimes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not belong here - I think it is best left in the TripTimes
with documentation:
NOTE! THIS IS A HACK TO SUPPORT ... GTFS-RT AND OTPs CONSTRAINT THAT ALL STOPS
NEED TO HAVE INCREASING TIMES.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method was in TripTimes
in the beginning, but you commented:
Consider configuring this feature per updater. This should be default off. If this is done, then no explicit exclude for SIRI is needed. Only GTFS updeters need the flag to turn it on.
That's why I moved it right next to the GTFS-RT method and made it private. Would you still want me to move it back to TripTimes
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there was a misunderstanding: in our language an updater is an individual feed source updater rather than then Siri/Gtfsrt split you thought about.
So, yes, please move it back into TripTimes. Sorry for the confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. I will move everything back then!
@@ -343,6 +352,12 @@ public Result<TripTimesPatch, UpdateError> createUpdatedTripTimes( | |||
} | |||
} | |||
|
|||
// Interpolate missing times from CANCELLED stops. Note: Currently for GTFS-RT ONLY. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Interpolate missing times from CANCELLED stops. Note: Currently for GTFS-RT ONLY. | |
// Interpolate missing times from CANCELLED stops. | |
// THIS IS A HACK TO SUPPORT THE OTP RESTRICTON THAT ALL TIMES NEED TO BE INCREASING. | |
// THIS IS DUE TO THE SUPPORT OF THE `includeRealtimeCancellations`. | |
// THIS ONLY APPLY TO GTFS, NOT SIRI. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also the JavaDoc on the method need to be updated. Consider renaming the method to createUpdatedTripTimesForGTFS
.
Hopefully we can address this and clean up this mess after the refactoring of the transit model.
…odified TripUpdateBuilder to be more explicit, renamed method to createUpdatedTripTimesFromGTFSRT to avoid confusion with SIRI.
Hello! Here's the latest commit. I moved the interpolation method back to TripTimes, added test cases, modified TripUpdateBuilder to be more explicit, renamed method to createUpdatedTripTimesFromGTFSRT to avoid confusion with SIRI. Let me know if there is anything else I forgot! |
Hello again! I just got a notification saying the build has failed. I checked the error log, and it says |
We had a problem last week with a annoyingly flaky test. It had nothing to do with your change. It has since been fixed and your build is green again. @t2gran will review your code when he gets to it. |
Thanks for the contribution, @wangyuxuan-symun! |
Summary
Relaxed the non-increasing time check when applying real-time updates for cancelled stops. Edit: removed the no_data case.
Issue
Issue 5286
#5286
Unit tests
Added 4 test cases in TripTimesTest class. You can find the explanation and expected results in the corresponding Javadoc.
All other existing tests passed.
Documentation
Modified the Javadoc for validateNonIncreasingTimes method to reflect the change.