From 8d6a184a1fc3a5e7e87583e4d772724447579807 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Thu, 6 May 2021 09:20:20 -0700 Subject: [PATCH] Speed up the 'find perfect home' tool by starting Dijkstra's floodfill with multiple sources. Only implemented for walking right now. --- fifteen_min/src/find_home.rs | 65 +++++++++++++-------------- fifteen_min/src/isochrone.rs | 11 ++--- map_model/src/connectivity/mod.rs | 10 +++-- map_model/src/connectivity/walking.rs | 31 ++++++++----- 4 files changed, 62 insertions(+), 55 deletions(-) diff --git a/fifteen_min/src/find_home.rs b/fifteen_min/src/find_home.rs index 5f4b6b9554..2ff8ba0076 100644 --- a/fifteen_min/src/find_home.rs +++ b/fifteen_min/src/find_home.rs @@ -1,6 +1,6 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; -use abstutil::{Counter, Parallelism, Timer}; +use abstutil::{prettyprint_usize, Counter, Parallelism, Timer}; use geom::Percent; use map_gui::tools::PopupMsg; use map_model::{AmenityType, BuildingID}; @@ -89,33 +89,28 @@ fn score_houses( options: Options, timer: &mut Timer, ) -> HashMap { + let num_categories = amenities.len(); let mut satisfied_per_bldg: Counter = Counter::new(); - // There may be more clever ways to calculate this, but we'll take the brute-force approach of - // calculating the walkshed from every matching business. - let num_categories = amenities.len(); - for category in amenities { - let mut stores: HashSet = HashSet::new(); - for b in app.map.all_buildings() { - if b.has_amenity(category) { - stores.insert(b.id); + let map = &app.map; + for times in timer.parallelize( + "find houses close to amenities", + Parallelism::Fastest, + amenities, + |category| { + // For each category, find all matching stores + let mut stores = Vec::new(); + for b in map.all_buildings() { + if b.has_amenity(category) { + stores.push(b.id); + } } - } - let mut houses: HashSet = HashSet::new(); - let map = &app.map; - for times in timer.parallelize( - &format!("find houses close to {}", category), - Parallelism::Fastest, - stores.into_iter().collect(), - |b| options.clone().time_to_reach_building(map, b), - ) { - for (b, _) in times { - houses.insert(b); - } - } - - for b in houses { + // Then find all buildings reachable from any of those starting points + options.clone().times_from_buildings(map, stores) + }, + ) { + for (b, _) in times { satisfied_per_bldg.inc(b); } } @@ -140,12 +135,22 @@ impl Results { scores: HashMap, amenities: Vec, ) -> Box> { + // TODO Show imperfect matches with different colors. + let mut batch = GeomBatch::new(); + let mut count = 0; + for (b, pct) in scores { + if pct == Percent::int(100) { + batch.push(Color::RED, app.map.get_b(b).polygon.clone()); + count += 1; + } + } + let panel = Panel::new(Widget::col(vec![ Line("Results for your walkable home") .small_heading() .into_widget(ctx), // TODO Adjust text to say bikeshed, or otherwise reflect the options chosen - "Here are all of the matching houses.".text_widget(ctx), + format!("{} houses match", prettyprint_usize(count)).text_widget(ctx), format!( "Containing at least 1 of each: {}", amenities @@ -164,14 +169,6 @@ impl Results { .aligned(HorizontalAlignment::RightInset, VerticalAlignment::TopInset) .build(ctx); - // TODO Show imperfect matches with different colors. - let mut batch = GeomBatch::new(); - for (b, pct) in scores { - if pct == Percent::int(100) { - batch.push(Color::RED, app.map.get_b(b).polygon.clone()); - } - } - SimpleState::new( panel, Box::new(Results { diff --git a/fifteen_min/src/isochrone.rs b/fifteen_min/src/isochrone.rs index adc4fd324b..b0be2f3d76 100644 --- a/fifteen_min/src/isochrone.rs +++ b/fifteen_min/src/isochrone.rs @@ -38,18 +38,19 @@ pub enum Options { } impl Options { - pub fn time_to_reach_building( + // TODO doc + pub fn times_from_buildings( self, map: &Map, - start: BuildingID, + starts: Vec, ) -> HashMap { match self { Options::Walking(opts) => { - connectivity::all_walking_costs_from(map, start, Duration::minutes(15), opts) + connectivity::all_walking_costs_from(map, starts, Duration::minutes(15), opts) } Options::Biking => connectivity::all_vehicle_costs_from( map, - start, + starts, Duration::minutes(15), PathConstraints::Bike, ), @@ -59,7 +60,7 @@ impl Options { impl Isochrone { pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID, options: Options) -> Isochrone { - let time_to_reach_building = options.clone().time_to_reach_building(&app.map, start); + let time_to_reach_building = options.clone().times_from_buildings(&app.map, vec![start]); let mut amenities_reachable = MultiMap::new(); let mut population = 0; diff --git a/map_model/src/connectivity/mod.rs b/map_model/src/connectivity/mod.rs index 7f738addd5..86cebd8d87 100644 --- a/map_model/src/connectivity/mod.rs +++ b/map_model/src/connectivity/mod.rs @@ -50,11 +50,12 @@ pub fn find_scc(map: &Map, constraints: PathConstraints) -> (HashSet, Ha (largest_group, disconnected) } -/// Starting from one building, calculate the cost to all others. If a destination isn't reachable, -/// it won't be included in the results. Ignore results greater than the time_limit away. +/// Starting from some initial buildings, calculate the cost to all others. If a destination isn't +/// reachable, it won't be included in the results. Ignore results greater than the time_limit +/// away. pub fn all_vehicle_costs_from( map: &Map, - start: BuildingID, + starts: Vec, time_limit: Duration, constraints: PathConstraints, ) -> HashMap { @@ -77,7 +78,8 @@ pub fn all_vehicle_costs_from( } } - if let Some(start_road) = bldg_to_road.get(&start) { + // TODO Use all starting points + if let Some(start_road) = bldg_to_road.get(&starts[0]) { let graph = build_graph_for_vehicles(map, constraints); let cost_per_road = petgraph::algo::dijkstra(&graph, *start_road, None, |(_, _, mvmnt)| { vehicle_cost(mvmnt.from, *mvmnt, constraints, map.routing_params(), map) diff --git a/map_model/src/connectivity/walking.rs b/map_model/src/connectivity/walking.rs index 51cf1d3760..64914db871 100644 --- a/map_model/src/connectivity/walking.rs +++ b/map_model/src/connectivity/walking.rs @@ -56,27 +56,34 @@ impl Ord for Item { } } -/// Starting from one building, calculate the cost to all others. If a destination isn't reachable, -/// it won't be included in the results. Ignore results greater than the time_limit away. +/// Starting from some initial buildings, calculate the cost to all others. If a destination isn't +/// reachable, it won't be included in the results. Ignore results greater than the time_limit +/// away. /// -/// If the start building is on the shoulder of a road and `!opts.allow_shoulders`, then the -/// results will always be empty. +/// If all of the start buildings are on the shoulder of a road and `!opts.allow_shoulders`, then +/// the results will always be empty. pub fn all_walking_costs_from( map: &Map, - start: BuildingID, + starts: Vec, time_limit: Duration, opts: WalkingOptions, ) -> HashMap { - let start_lane = map.get_l(map.get_b(start).sidewalk_pos.lane()); - if start_lane.lane_type == LaneType::Shoulder && !opts.allow_shoulders { - return HashMap::new(); + if !opts.allow_shoulders { + if starts + .iter() + .all(|b| map.get_l(map.get_b(*b).sidewalk()).lane_type == LaneType::Shoulder) + { + return HashMap::new(); + } } let mut queue: BinaryHeap = BinaryHeap::new(); - queue.push(Item { - cost: Duration::ZERO, - node: WalkingNode::closest(map.get_b(start).sidewalk_pos, map), - }); + for b in starts { + queue.push(Item { + cost: Duration::ZERO, + node: WalkingNode::closest(map.get_b(b).sidewalk_pos, map), + }); + } let mut cost_per_node: HashMap = HashMap::new(); while let Some(current) = queue.pop() {