Speed up the 'find perfect home' tool by starting Dijkstra's floodfill with multiple sources. Only implemented for walking right now.

This commit is contained in:
Dustin Carlino 2021-05-06 09:20:20 -07:00
parent b29a613b42
commit 8d6a184a1f
4 changed files with 62 additions and 55 deletions

View File

@ -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 geom::Percent;
use map_gui::tools::PopupMsg; use map_gui::tools::PopupMsg;
use map_model::{AmenityType, BuildingID}; use map_model::{AmenityType, BuildingID};
@ -89,33 +89,28 @@ fn score_houses(
options: Options, options: Options,
timer: &mut Timer, timer: &mut Timer,
) -> HashMap<BuildingID, Percent> { ) -> HashMap<BuildingID, Percent> {
let num_categories = amenities.len();
let mut satisfied_per_bldg: Counter<BuildingID> = Counter::new(); let mut satisfied_per_bldg: Counter<BuildingID> = 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<BuildingID> = HashSet::new();
for b in app.map.all_buildings() {
if b.has_amenity(category) {
stores.insert(b.id);
}
}
let mut houses: HashSet<BuildingID> = HashSet::new();
let map = &app.map; let map = &app.map;
for times in timer.parallelize( for times in timer.parallelize(
&format!("find houses close to {}", category), "find houses close to amenities",
Parallelism::Fastest, Parallelism::Fastest,
stores.into_iter().collect(), amenities,
|b| options.clone().time_to_reach_building(map, b), |category| {
) { // For each category, find all matching stores
for (b, _) in times { let mut stores = Vec::new();
houses.insert(b); for b in map.all_buildings() {
if b.has_amenity(category) {
stores.push(b.id);
} }
} }
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); satisfied_per_bldg.inc(b);
} }
} }
@ -140,12 +135,22 @@ impl Results {
scores: HashMap<BuildingID, Percent>, scores: HashMap<BuildingID, Percent>,
amenities: Vec<AmenityType>, amenities: Vec<AmenityType>,
) -> Box<dyn State<App>> { ) -> Box<dyn State<App>> {
// 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![ let panel = Panel::new(Widget::col(vec![
Line("Results for your walkable home") Line("Results for your walkable home")
.small_heading() .small_heading()
.into_widget(ctx), .into_widget(ctx),
// TODO Adjust text to say bikeshed, or otherwise reflect the options chosen // 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!( format!(
"Containing at least 1 of each: {}", "Containing at least 1 of each: {}",
amenities amenities
@ -164,14 +169,6 @@ impl Results {
.aligned(HorizontalAlignment::RightInset, VerticalAlignment::TopInset) .aligned(HorizontalAlignment::RightInset, VerticalAlignment::TopInset)
.build(ctx); .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( SimpleState::new(
panel, panel,
Box::new(Results { Box::new(Results {

View File

@ -38,18 +38,19 @@ pub enum Options {
} }
impl Options { impl Options {
pub fn time_to_reach_building( // TODO doc
pub fn times_from_buildings(
self, self,
map: &Map, map: &Map,
start: BuildingID, starts: Vec<BuildingID>,
) -> HashMap<BuildingID, Duration> { ) -> HashMap<BuildingID, Duration> {
match self { match self {
Options::Walking(opts) => { 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( Options::Biking => connectivity::all_vehicle_costs_from(
map, map,
start, starts,
Duration::minutes(15), Duration::minutes(15),
PathConstraints::Bike, PathConstraints::Bike,
), ),
@ -59,7 +60,7 @@ impl Options {
impl Isochrone { impl Isochrone {
pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID, options: Options) -> 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 amenities_reachable = MultiMap::new();
let mut population = 0; let mut population = 0;

View File

@ -50,11 +50,12 @@ pub fn find_scc(map: &Map, constraints: PathConstraints) -> (HashSet<LaneID>, Ha
(largest_group, disconnected) (largest_group, disconnected)
} }
/// Starting from one building, calculate the cost to all others. If a destination isn't reachable, /// Starting from some initial buildings, calculate the cost to all others. If a destination isn't
/// it won't be included in the results. Ignore results greater than the time_limit away. /// reachable, it won't be included in the results. Ignore results greater than the time_limit
/// away.
pub fn all_vehicle_costs_from( pub fn all_vehicle_costs_from(
map: &Map, map: &Map,
start: BuildingID, starts: Vec<BuildingID>,
time_limit: Duration, time_limit: Duration,
constraints: PathConstraints, constraints: PathConstraints,
) -> HashMap<BuildingID, Duration> { ) -> HashMap<BuildingID, Duration> {
@ -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 graph = build_graph_for_vehicles(map, constraints);
let cost_per_road = petgraph::algo::dijkstra(&graph, *start_road, None, |(_, _, mvmnt)| { let cost_per_road = petgraph::algo::dijkstra(&graph, *start_road, None, |(_, _, mvmnt)| {
vehicle_cost(mvmnt.from, *mvmnt, constraints, map.routing_params(), map) vehicle_cost(mvmnt.from, *mvmnt, constraints, map.routing_params(), map)

View File

@ -56,27 +56,34 @@ impl Ord for Item {
} }
} }
/// Starting from one building, calculate the cost to all others. If a destination isn't reachable, /// Starting from some initial buildings, calculate the cost to all others. If a destination isn't
/// it won't be included in the results. Ignore results greater than the time_limit away. /// 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 /// If all of the start buildings are on the shoulder of a road and `!opts.allow_shoulders`, then
/// results will always be empty. /// the results will always be empty.
pub fn all_walking_costs_from( pub fn all_walking_costs_from(
map: &Map, map: &Map,
start: BuildingID, starts: Vec<BuildingID>,
time_limit: Duration, time_limit: Duration,
opts: WalkingOptions, opts: WalkingOptions,
) -> HashMap<BuildingID, Duration> { ) -> HashMap<BuildingID, Duration> {
let start_lane = map.get_l(map.get_b(start).sidewalk_pos.lane()); if !opts.allow_shoulders {
if start_lane.lane_type == LaneType::Shoulder && !opts.allow_shoulders { if starts
.iter()
.all(|b| map.get_l(map.get_b(*b).sidewalk()).lane_type == LaneType::Shoulder)
{
return HashMap::new(); return HashMap::new();
} }
}
let mut queue: BinaryHeap<Item> = BinaryHeap::new(); let mut queue: BinaryHeap<Item> = BinaryHeap::new();
for b in starts {
queue.push(Item { queue.push(Item {
cost: Duration::ZERO, cost: Duration::ZERO,
node: WalkingNode::closest(map.get_b(start).sidewalk_pos, map), node: WalkingNode::closest(map.get_b(b).sidewalk_pos, map),
}); });
}
let mut cost_per_node: HashMap<WalkingNode, Duration> = HashMap::new(); let mut cost_per_node: HashMap<WalkingNode, Duration> = HashMap::new();
while let Some(current) = queue.pop() { while let Some(current) = queue.pop() {