fix: Check StopTooFarFromTripShape once per trip stoptimes - #750
Conversation
* Only process each trip once * Refactor forEach to simple loop to clarify behavior * Refactor distance variables and method to use double instead of float and int * Refactor utility method signature to remove need for trip object * Cleanup docs - remove references to "error" * Add docs for method of distance calculation * Fix existing tests
lionel-nj
left a comment
There was a problem hiding this comment.
Just one comment in-line
…/gtfs-validator into fix-stop-too-far-from-shape
- draft one more unit test to verify the number of method invocations - make checkStopsWithinTripShape package private
- draft one more unit test to verify the number of method invocations - make checkStopsWithinTripShape package private
|
@barbeau I drafted an additional unit to verify the number of invocation on The detailed error message:
Here, I think that this set should be empty since it is the first call to
Here I think that this set should be To make sure that |
| } | ||
|
|
||
| // Create a polyline from the GTFS shapes data | ||
| ShapeFactory.LineStringBuilder lineBuilder = getShapeFactory().lineString(); |
There was a problem hiding this comment.
Is there a method to preallocate the size needed for all shapePoints? Something like lineBuilder.setCapacity(shapePoints.size())
There was a problem hiding this comment.
Good idea, but unfortunately no :(. I looked at the source of spatial4j, and it has a comment in the builder class:
// TODO add dimensionality hint method?
Looks like it's backed by an ArrayList, so a lot of re-sizing will happen here :(
There was a problem hiding this comment.
In good news, in looking at the library source I realized that you can specify a buffered line directly in the lineBuilder, without having to build the line first and then generating the buffer. So that saves memory and execution time for each trip shape.
However, I tested the change and initially got some confusing results. The unit test (using an original shape to line distance of around 140m - the threshold is 100m) passed with the change, but I ran it against MBTA's dataset with the old and new code, and unexpectedly got different results - directly buffering the line in the builder gave me 8 notices, while creating the second instance (original code) gave me 6 notices.
After more poking around in spatial4j code, it looks like the difference is that when buffering within ShapeFactory.LineStringBuilder directly, it always returns a non-geodesic (straight-line) buffer. Conversely, when you first build and then separately buffer the line, it always returns a geodesic buffer (i.e., one that follows the surface of the earth).
The differences between the two are slight, but enough to cause differences in results on real world datasets. I manually measured the distance between one of the stops and the shape in the newly generated notice using Google Earth Pro and it measures around 106 meters.
Another consideration - as you might expect, I noticed that execution time on Euclidean buffers seems to be consistently less than on geodesic buffers. Here are some measurements running on my laptop from within IntelliJ.
Execution time - Euclidean (seconds):
- 15
- 11
- 12
- 13
- 11
- 12
Execution time - Geodesic (seconds):
- 21
- 18
- 17
- 16
- 17
- 17
- 16
Given what seems to be a decent performance increase in execution time alone (memory performance should be better too, but I didn't measure it), I'm inclined to go with the Euclidean measurements. For our purposes I think the small error doesn't really matter - our intent is to draw attention to potential GTFS errors with stops far from the trip shape, and if we flag one that's a few meters off it shouldn't be problematic. I've made this change in 58718de.
| * Validates: a {@link GtfsStop} is within a distance threshold for a trip shape. | ||
| * | ||
| * <p>Generated notice: {@link StartAndEndDateOutOfOrderNotice}. | ||
| * <p>Generated notice: {@link StopTooFarFromTripShapeNotice}. |
There was a problem hiding this comment.
Could you also give time complexity of this algoritm?
There was a problem hiding this comment.
I've added notes on time complexity in e47ee19. I actually had to think about this a bit, and hopefully I've expressed it in a way that's understandable. Improvements welcome!
@lionel-nj Your expectations are right, and match actual execution when you set a debugger breakpoint within |
Mockito captures parameters by reference and not value, so we get the final state of the cache, not the state after each method invocation
So it seems that Mockito captures parameters for each method invocation by reference, so as a result after execution when we check the contents of the cache it's the final contents after all execution finishes, not the contents after each method invocation. I pushed a change in 70213ac that allows us to test each invocation separately without testing the cache, and then have a final cache test with a note in comments on the above. This fixes the test. |
|
Wow, good to know! Thanks @barbeau! |
stop_times.txt should only have stops location_types 0 and 4, so validation for that can be done elsewhere.
stop_times.txt should only have stops location_types 0 and 4, so validation for that can be done elsewhere.
This also has the effect of using Euclidean instead of Geodetic buffers, which saves execution time and likely memory, and doesn't seem to result in a level of error that causes problems. So the tradeoff of performance vs. accuracy seems acceptable. See #750 (comment) for details.
|
This still can be significantly optimized because we need to validate each line variant only once (line variant is effectively a trip without time information). However, we can submit it as-is and improve later. |
|
@aababilov I agree - I opened #766 to discuss the line variant optimizations. |
Summary:
Change to check StopTooFarFromTripShape once per trip stoptimes, plus a number of other improvements:
Closes #747
@lionel-nj Please review and also add the additional unit tests we discussed. Thanks!
Expected behavior:
Only check each trip once, and only check the stop_times.txt for that trip once
Please make sure these boxes are checked before submitting your pull request - thanks!
gradle testto make sure you didn't break anything