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

Enable mode-specific transfers by storing mode information in transfers #6293

Open
wants to merge 13 commits into
base: dev-2.x
Choose a base branch
from

Conversation

VillePihlava
Copy link
Contributor

@VillePihlava VillePihlava commented Nov 29, 2024

Summary

This PR adds StreetMode information to transfers. This enables using mode-specific transfers during runtime.

Notes:

  • Instead of a collection of PathTransfers with mode information, this could also be done by using mappings from modes to transfers.
    • Depending on how this is done, this could maybe be cheaper in terms of memory?
    • In my tests the size of the graph has a similar size with both implementations. I prefer the implementation in this PR
  • The size of the graph can increase a lot when using a large dataset and when using CAR transfers.
    • When I tested building the graph with data from almost the entirety of Finland, the size of the graph file increased from 1.4G to 6.9G.
    • When using car transfers, because the transfers are calculated based on a range by duration there can be a lot of transfers.
    • The solution is to use CAR transfers only with relevant stops.
    • I will create a PR in the future for optionally limiting which stops can use transfers with cars.
    • Using an EnumSet was the cheapest way I could think of adding this functionality when having the information in PathTransfer.
  • For large datasets with many transfers, build duration can increase.
    • When I tested building the graph with data from almost the entirety of Finland, build duration doubled.
    • For less transfers, build duration does not increase as much.

Issue

Resolves a TODO comment about adding StreetMode information in the PathTransfer class.

Related to #5875

Unit tests

Added a new test to DirectTransferGeneratorTest.

Documentation

No documentation has been added.

Bumping the serialization version id

This changes how transfers are generated during the graph build.

@VillePihlava VillePihlava requested a review from a team as a code owner November 29, 2024 13:57
@VillePihlava
Copy link
Contributor Author

The tests will fail for CarPickupSnapshotTest. There is discussion related to changing the test in #6238. This PR should not be merged before that test has been changed and the tests pass.

@VillePihlava
Copy link
Contributor Author

Interestingly FlexIntegrationTest fails as well. I was able to locally run this test and all other tests except CarPickupSnapshotTest successfully, but I have noticed some flakiness related to running that test and running tests in general.

@VillePihlava
Copy link
Contributor Author

Interestingly now after making changes not related to functionality, the FlexIntegrationTest did not fail.

Copy link

codecov bot commented Dec 4, 2024

Codecov Report

Attention: Patch coverage is 78.20513% with 17 lines in your changes missing coverage. Please review.

Project coverage is 69.81%. Comparing base (69e6f64) to head (95e9ca7).
Report is 101 commits behind head on dev-2.x.

Files with missing lines Patch % Lines
...apping/StreetModeToTransferTraverseModeMapper.java 14.28% 5 Missing and 1 partial ⚠️
...pentripplanner/api/parameter/QualifiedModeSet.java 0.00% 3 Missing ⚠️
.../graph_builder/module/DirectTransferGenerator.java 91.17% 1 Missing and 2 partials ⚠️
...raptoradapter/transit/mappers/TransfersMapper.java 71.42% 2 Missing ⚠️
...t/java/org/opentripplanner/ext/flex/FlexIndex.java 75.00% 1 Missing ⚠️
.../java/org/opentripplanner/ext/flex/FlexRouter.java 0.00% 1 Missing ⚠️
...n/java/org/opentripplanner/model/PathTransfer.java 87.50% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #6293      +/-   ##
=============================================
+ Coverage      69.79%   69.81%   +0.02%     
- Complexity     17788    17836      +48     
=============================================
  Files           2017     2020       +3     
  Lines          76042    76297     +255     
  Branches        7781     7804      +23     
=============================================
+ Hits           53074    53270     +196     
- Misses         20264    20311      +47     
- Partials        2704     2716      +12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment on lines 137 to 145
if (pathTransfer == null) {
EnumSet<StreetMode> modes = EnumSet.of(mode);
distinctTransfers.put(
transferKey,
new PathTransfer(stop, sd.stop, sd.distance, sd.edges, modes)
);
} else {
pathTransfer.addMode(mode);
}
Copy link
Member

Choose a reason for hiding this comment

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

You could add a comment here to explain what these different cases mean.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added comments for the different cases

}
// Calculate flex transfers if flex routing is enabled.
for (RouteRequest transferProfile : flexTransferRequests) {
StreetMode mode = transferProfile.journey().transfer().mode();
Copy link
Member

Choose a reason for hiding this comment

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

Is this just always WALK?

Copy link
Member

Choose a reason for hiding this comment

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

We have always assumed that flex transfers can only be on foot. I think this is reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed this to StreetMode mode = StreetMode.WALK;

@@ -250,6 +250,61 @@ public void testMultipleRequestsWithPatterns() {
);
}

@Test
public void testPathTransfersWithModesForMultipleRequestsWithPatterns() {
Copy link
Member

Choose a reason for hiding this comment

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

What does multiple requests with patterns mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to follow a similar naming convention as the other tests in the file. With patterns means that trippatterns are added to the timetablerepository of the test. Multiple requests means that transfers are generated for multiple requests that have different modes:

var reqWalk = new RouteRequest();
reqWalk.journey().transfer().setMode(StreetMode.WALK);

var reqBike = new RouteRequest();
reqBike.journey().transfer().setMode(StreetMode.BIKE);

var transferRequests = List.of(reqWalk, reqBike);

this.toStop = toStop;
this.edges = edges;
this.distanceMeters = (int) edges.stream().mapToDouble(Edge::getDistanceMeters).sum();
this.modes = modes;
Copy link
Member

Choose a reason for hiding this comment

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

We probably want to make this immutable. EnumSet are not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed the getModes method to clone the enum set

Comment on lines 277 to 291
var walkTransfers = timetableRepository
.getAllPathTransfers()
.stream()
.filter(pathTransfer -> pathTransfer.getModes().contains(StreetMode.WALK))
.toList();
var bikeTransfers = timetableRepository
.getAllPathTransfers()
.stream()
.filter(pathTransfer -> pathTransfer.getModes().contains(StreetMode.BIKE))
.toList();
var carTransfers = timetableRepository
.getAllPathTransfers()
.stream()
.filter(pathTransfer -> pathTransfer.getModes().contains(StreetMode.CAR))
.toList();
Copy link
Member

Choose a reason for hiding this comment

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

please extract a method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created a getTransfersByMode method in TimetableRepository

@leonardehrenfried
Copy link
Member

  • The solution is to use CAR transfers only with relevant stops.

Does this mean that this PR allows you to compute CAR transfers between stops that don't serve cars at all?

@@ -41,6 +43,7 @@ public static RaptorTransferIndex create(
var transfers = transfersByStopIndex
.get(fromStop)
.stream()
.filter(transfer -> transfer.getModes().contains(mode))
Copy link
Member

Choose a reason for hiding this comment

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

This is run only once and then cached for the specific combination of request parameters, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is run once for each cache request. You can see the code that uses this in RaptorRequestTransferCache

return this.modes;
}

public boolean addMode(StreetMode mode) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we want to make this class mutable. Can you create a copy method, for example withAddedMode that returns a new instance instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed this to use the withAddedMode method

@@ -46,6 +56,14 @@ public List<Edge> getEdges() {
return this.edges;
}

public EnumSet<StreetMode> getModes() {
Copy link
Member

Choose a reason for hiding this comment

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

We don't want to expose the mutable EnumSet. Can you replace it with a method like allows(CAR)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed the getModes method to clone the enum set

transfersToStop.put(transfer.to, transfer);
transfersFromStop.put(transfer.from, transfer);
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain why this is now necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is not strictly necessary, but instead it limits the available transfers to the WALK mode when searching for fromstops for flex transfers. Previously all of the transfers for a stop, including non-walk mode transfers, were given when using the getTransfersFromStop method in the FlexRouter. This was because it used the findPathTransfers method which gives all transfers for a given stop.

I think this could use my current implementation or the old one. I do have a somewhat shallow understanding of flex routing at the moment, but I went with this implementation because it seems more correct.

Comment on lines 211 to 212
.map(transferProfile -> transferProfile.journey().transfer().mode())
.collect(Collectors.toSet())) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
.map(transferProfile -> transferProfile.journey().transfer().mode())
.collect(Collectors.toSet())) {
.map(transferProfile -> transferProfile.journey().transfer().mode())
.distinct()
.toList()

Not a huge deal but avoiding a set gives you predictable sort order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, this is a good suggestion. I changed it to this

@VillePihlava
Copy link
Contributor Author

Does this mean that this PR allows you to compute CAR transfers between stops that don't serve cars at all?

Yes, in a future PR (#6215) I will implement more configurability for transfers

}

public EnumSet<StreetMode> getModes() {
return modes.clone();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return modes.clone();
return EnumSet.copyOf(modes);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed this

this.toStop = toStopIndex;
this.distanceMeters = distanceMeters;
this.edges = null;
this.modes = modes;
Copy link
Member

Choose a reason for hiding this comment

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

This transfer is not serialized. Here you can use Sets.immutableEnumSet and then never have to worry about cloning or copying.

Copy link
Contributor Author

@VillePihlava VillePihlava Dec 9, 2024

Choose a reason for hiding this comment

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

I changed this to use Sets.immutableEnumSet

Comment on lines 212 to 222
for (StreetMode mode : transferRequests
.stream()
.map(transferProfile -> transferProfile.journey().transfer().mode())
.distinct()
.toList()) {
LOG.info(
"Created {} transfers for mode {}.",
transfersByStop
.values()
.stream()
.filter(pathTransfer -> pathTransfer.getModes().contains(mode))
.count(),
mode
);
Copy link
Member

@optionsome optionsome Dec 10, 2024

Choose a reason for hiding this comment

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

This is quite difficult to read. I would probably continue the stream instead of making it a list and then looping over it, and then call some method with the mode as an argument that does the logging. So something like:

transferRequests
      .stream()
      .map(transferProfile -> transferProfile.journey().transfer().mode())
      .distinct()
      .peek(mode -> logTransfersForMode(mode));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed it to this:

transferRequests
      .stream()
      .map(transferProfile -> transferProfile.journey().transfer().mode())
      .distinct()
      .forEach(mode ->
        LOG.info(
          "Created {} transfers for mode {}.",
          timetableRepository.getTransfersByMode(mode).size(),
          mode
        )
      );

@miklcct
Copy link
Contributor

miklcct commented Dec 10, 2024

I am following this as I am now testing BICYCLE routing on our deployment. The current deployment configuration only has "walk" with PT15M in build-config.json and the bicycle routing is near-useless for cross-London journeys, as it can't return results cycling from one London terminal to another (some of these transfers require about 30 minutes on the bike).

However, if I set the build-config.json to do walk and bike transfers with PT30M duration, the graph has become 15.1 GB just for London only without any other parts of Great Britain, and the transfer cache takes forever to build rather than seconds that it can't even cope with a city-sized deployment, and I can't even think of how it can work when put on a country-wide deployment.

@VillePihlava
Copy link
Contributor Author

I am following this as I am now testing BICYCLE routing on our deployment. The current deployment configuration only has "walk" with PT15M in build-config.json and the bicycle routing is near-useless for cross-London journeys, as it can't return results cycling from one London terminal to another (some of these transfers require about 30 minutes on the bike).

However, if I set the build-config.json to do walk and bike transfers with PT30M duration, the graph has become 15.1 GB just for London only without any other parts of Great Britain, and the transfer cache takes forever to build rather than seconds that it can't even cope with a city-sized deployment, and I can't even think of how it can work when put on a country-wide deployment.

Without knowing your deployment in more detail, this sounds like there is something wrong with your configuration. While this PR limits the transfers to a specific mode thus maybe improving performance, the main reason for implementing this PR was to remove weird transfers that were relevant for a certain mode but not necessarily for another.

Therefore I suggest looking more closely at your configuration for performance improvements. A large graph size because of transfers could be caused by, for example, lots of transfers or transfers with lots of edges.

@miklcct
Copy link
Contributor

miklcct commented Dec 10, 2024

How many transfers are in your graph? If I enable 45-minute transfers on bikes, it will generate approximately 15 million transfers just in London (as there are about 44k stops in London - however only thousands will actually serve bikes)! However, the majority of these transfers are useless and only those stops which have bike permitting services (a minority of the stops) are relevant, and without change, the graph is so large that it is flooded with useless transfers to the extent that it is unusable.

Long transfers are needed to enable people cycling across London to connect between intercity trains when local trains do not run between London terminals, or between pairs of terminals where no local trains run.

@VillePihlava
Copy link
Contributor Author

I do not remember the amount of transfers off the top of my head. However, as I have written in this PR the graph for the entirety of Finland is ~1.4gb (not the exact number but in the same ballpark) and when the transfers were badly configured with the CAR mode, the same graph was ~6.9gb.

@miklcct
Copy link
Contributor

miklcct commented Dec 10, 2024

I do not remember the amount of transfers off the top of my head. However, as I have written in this PR the graph for the entirety of Finland is ~1.4gb (not the exact number but in the same ballpark) and when the transfers were badly configured with the CAR mode, the same graph was ~6.9gb.

A graph with 45 minutes walk and bicycle transfers is 15 GB just for Greater London.

I am afraid it may grow to hundreds of GB if I attempt to pull this off in Hong Kong (where the stop density is multiple times higher than London).

@VillePihlava VillePihlava force-pushed the split-transfers-by-mode-pathtransfer-mode branch from a68ad75 to 3ae6eed Compare December 11, 2024 14:44
@@ -434,7 +435,7 @@ public Collection<PathTransfer> getTransfersByStop(StopLocation stop) {
return transfersByStop.get(stop);
}

public Collection<PathTransfer> getTransfersByMode(StreetMode mode) {
public List<PathTransfer> getTransfersByMode(StreetMode mode) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this method should be called findTransfers according to the naming conventions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I renamed getTransfersByMode to findTransfers

@leonardehrenfried
Copy link
Member

@t2gran can we run the speed tests against that? Do you know how to do it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants