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

Simplify pathfinding with zones #582

Merged
merged 3 commits into from
Mar 22, 2021
Merged

Simplify pathfinding with zones #582

merged 3 commits into from
Mar 22, 2021

Conversation

dabreegster
Copy link
Collaborator

Background

Access-restricted zones (just "zones" after this) are meant to represent a few situations where "through-traffic" is restricted:

  • low traffic neighborhoods (in the UK)
  • stay healthy streets (Seattle)
  • private/gated neighborhoods (Broadmoor in Seattle)

They're the funny pink colored roads:
Screenshot from 2021-03-22 13-45-34

If you edit one of them and "change access restrictions", you see the explanation of how they work:
Screenshot from 2021-03-22 13-45-56

"No through-traffic" means a trip can't cut through the zone. Access only works if the trip begins or ends in that zone. This captures the real-world semantics: residents of that zone can always go in/out, people delivering or visiting can go there, but otherwise, nobody can enter.

The rules for through-traffic are further broken down by mode. For Stay Healthy Streets, for example, the point is to encourage foot/bike traffic and discourage car traffic.

Current implementation

When I implemented the pathfinding for zones last spring, for some reason I took an all-or-nothing approach. The graph that either Mr. Dijkstra searches or that feed into the contraction hierarchies omit roads inside of a zone. When map.pathfind runs, it sees if the request starts or ends in a zone. If so, it looks through all of the "borders" into/out of a zone, finds paths that fulfill the overall request, and delegates to a Dijkstra's impl to pathfind within a zone. It's complicated. There's some very hairy code to glue the pieces of the path together, especially for walking. It's not even a complete impl -- requesting to go from zone1 to a different zone2 is just unimplemented.

New implementation

This PR first rips out all of the complicated old stuff, then does something much simpler: penalizes entry into a zone at all turns crossing from outside a zone into the zone. If this penalty is high enough, then the same effect ought to be achieved -- through traffic routes around it naturally, but if a request starts or ends in the zone, then it has to incur that cost to make the graph search work at all.

Benefits

  • Radically simplifies the code. This is particularly useful to do to prepare for Pathfinding v2 / Exit driveways onto any lane #555, which will overhaul pathfinding completely from using lanes to using roads. Getting rid of all this special casing and path gluing makes the first step of that project much easier.
  • Better performance. We can just use CHs normally now; we don't need to check if a request starts/ends in a zone, separately runs Dijkstra's, and glue anything.
  • It handles the zone1 -> zone2 case without any particular code/effort needed.
  • It's a solid step towards fulfilling Traffic Diverters #488, which asks to explicitly model traffic diverters. I'm not sure how these will differ from zones, but expressing entry into the zone as a big penalty feels like it maps onto the concept of a diverter pretty well. A car can drive around a Stay Healthy St barrier, but there's some penalty discouraging it.

Validation of this change

Individual

I used the "start new trip" tool to request some individual paths in the Arboretum map. I loaded the "broadmoor access" edits and verified that car trips through Broadmoor still don't happen, but bike/foot trips do.

While doing this, I kept seeing a few cases with unexpected behavior. One example is:
Screenshot from 2021-03-22 14-00-23
Even with Broadmoor opened to bike traffic, this path doesn't make use of it. I created the new "Debug all costs" tool to figure out why. It prints the total cost of the chosen path, then floodfills from the start and tracks the cost at all roads, and shows that in tooltips. If you hover over roads near Broadmoor, you can sort of trace where the expected path becomes more expensive than the chosen one. In this case, I realized the problem is the side of the road that the destination is on. The cost of making a loop to finish at the building from the right side of the road legitimately does bump the cost up to prefer going through Lake Wash Blvd. Choosing a different building indeed makes bikes use the shortcut. This silliness will be fixed with #555 eventually.

In aggregate

Before regenerating prebaked results, I enabled the new pathfinding and looked at the sim to make sure people weren't suddenly flooding through Broadmoor. Except... oops, they were. I was confused by this, but as I checked individual people cutting through, they were all valid cases starting/ending in Broadmoor. Any guesses?

... The number of cancelled trips was the hint. Before this change, about 3400 trips fail. After, only 2600. Lots of the trips were failing before were hitting some bug with the previous zone pathfinding impl:
p4685_failed_before
P4685 succeeds after the change. So that explains the increased traffic through Broadmoor.

TODO: Regenerate all maps and make sure there are no gridlock regressions. Running that now.

Sanity checking

I added validate_zones to check every single path that gets created. It makes sure a path doesn't enter a zone unless the destination is in that zone. But it has lots of false positives, because of stuff like this:
Screenshot from 2021-03-22 14-07-17
If you start in the circled roads, you have no choice but to cross through a zone to reach most of the map. This is an artifact of how the access restrictions are tagged in OSM. Technically those inner roads are also private, but they're not always mapped that way.

Ideally this'd be fixed at map import time. We could look for roads that can't reach most of the graph without crossing a zone, and fold them into that zone. But meh, not urgent.

…stricted zones.

There's a much simpler implementation, it transpires. #555, #574
@dabreegster dabreegster requested a review from michaelkirk March 22, 2021 21:12
Copy link
Collaborator

@michaelkirk michaelkirk left a comment

Choose a reason for hiding this comment

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

Changes look great! It's easy to review a bunch of deletes.

And the rationale/plan laid out in your PR description all sounds good.

@@ -63,94 +59,6 @@ impl Zone {

zones
}

/// Run slower Dijkstra's within the interior of a private zone. Don't go outside the borders.
Copy link
Collaborator

Choose a reason for hiding this comment

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

🔥 🔥 🔥

if req.constraints == PathConstraints::Pedestrian {
if req.start.lane() == req.end.lane() {
return Some(one_step_walking_path(&req, map));
}
let steps = walking_path_to_steps(self.simple_walking_path(&req, map)?, map);
let nodes = match self {
Pathfinder::Dijkstra => dijkstra::simple_walking_path(&req, map)?,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you elaborate on when we expect to be using Dijkstra at this point?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Only if you import a map passing --skip_ch. None of the "official" maps in S3 use this; I just do it to debug occasionally. I could add a comment here, but the docstring on Pathfinder above sort of answers this.

... One more slight exception, pathfind_avoiding_lanes uses Dijkstra always. This gets called when congestion caps (an experimental feature) are applied through map edits and/or flags to the sim. I'll add a comment there.

Copy link
Collaborator

Choose a reason for hiding this comment

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

gotcha, thanks!

@@ -147,193 +93,4 @@ impl Pathfinder {
Pathfinder::CH(ref mut p) => p.apply_edits(map, timer),
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

🔥 🔥 🔥

@@ -10,6 +10,7 @@ use abstutil::MultiMap;

Copy link
Collaborator

Choose a reason for hiding this comment

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

minor nit: Apparently this is used by bikes too? Can we rename this module to pathfind::vehicle?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good, I'll do that in a followup.

Relatedly, I need to rename CarID to VehicleID and Bus* (ID, Stop, Route...) to PublicTransit* (buses or trains).

…rossing into the zones. #555, #574

Regenerate all maps. Gridlock-wise, Rainier and Poundbury broke, but
Wallingford started working again. Acceptable cost for a change this
useful; I'll work on fixing those maps later.
@dabreegster
Copy link
Collaborator Author

I'm uploading the 1.4 GB (sigh) of changes to S3 now. The Broadmoor proposal still has roughly the same expected effects. Rainier and Poundbury start gridlocking with this change, but Wallingford stops. I'll plunge ahead and fix those later. Very rough analysis: they start gridlocking because less trips get cancelled.

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.

2 participants