Express all pathfinding costs in units of seconds. #82, #494 (#587)

This is simpler to reason about, allows the penalty for entering a zone
or taking an unprotected turn to be expressed in terms of a time
penalty, and is a step towards adjusting bike/foot routing for elevation
data.

When we later add things like "safety/quietness" for cycling, maybe we
can switch to using a (time, quietness) tuple, and transform into a
single number with a linear combination parameterized by that agent's
preference for time/safety. This change is compatible with that future
idea.

There are behavior changes here, particularly for zones and unprotected
turns. No new maps start gridlocking, and in fact, Rainier starts
working again.
This commit is contained in:
Dustin Carlino 2021-03-25 12:59:36 -07:00 committed by GitHub
parent 4c2bc89438
commit 92d3a890ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 413 additions and 396 deletions

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@ pub fn prebake_all() {
MapName::seattle("lakeslice"),
MapName::seattle("phinney"),
MapName::seattle("qa"),
//MapName::seattle("rainier_valley"), // TODO broken
MapName::seattle("rainier_valley"),
MapName::seattle("wallingford"),
] {
let map = map_model::Map::load_synchronously(name.path(), &mut timer);

View File

@ -1,7 +1,7 @@
use std::collections::HashMap;
use abstutil::{prettyprint_usize, Counter, Parallelism, Timer};
use geom::Polygon;
use geom::{Duration, Polygon};
use map_gui::colors::ColorSchemeChoice;
use map_gui::tools::ColorNetwork;
use map_gui::{AppLike, ID};
@ -208,7 +208,7 @@ fn params_to_controls(ctx: &mut EventCtx, mode: TripMode, params: &RoutingParams
Spinner::widget(
ctx,
(1, 100),
(params.unprotected_turn_penalty * 10.0) as isize,
params.unprotected_turn_penalty.inner_seconds() as isize,
)
.named("unprotected turn penalty"),
]));
@ -237,13 +237,15 @@ fn params_to_controls(ctx: &mut EventCtx, mode: TripMode, params: &RoutingParams
fn controls_to_params(panel: &Panel) -> (TripMode, RoutingParams) {
let mut params = RoutingParams::default();
if !panel.is_button_enabled("cars") {
params.unprotected_turn_penalty = panel.spinner("unprotected turn penalty") as f64 / 10.0;
params.unprotected_turn_penalty =
Duration::seconds(panel.spinner("unprotected turn penalty") as f64);
return (TripMode::Drive, params);
}
if !panel.is_button_enabled("pedestrians") {
return (TripMode::Walk, params);
}
params.unprotected_turn_penalty = panel.spinner("unprotected turn penalty") as f64 / 10.0;
params.unprotected_turn_penalty =
Duration::seconds(panel.spinner("unprotected turn penalty") as f64 / 10.0);
params.bike_lane_penalty = panel.spinner("bike lane penalty") as f64 / 10.0;
params.bus_lane_penalty = panel.spinner("bus lane penalty") as f64 / 10.0;
params.driving_lane_penalty = panel.spinner("driving lane penalty") as f64 / 10.0;
@ -479,7 +481,7 @@ fn cmp_count(after: usize, before: usize) -> Vec<TextSpan> {
/// one start.
pub struct PathCostDebugger {
draw_path: Drawable,
costs: HashMap<RoadID, f64>,
costs: HashMap<RoadID, Duration>,
tooltip: Option<Text>,
panel: Panel,
}
@ -516,8 +518,11 @@ impl State<App> for PathCostDebugger {
if ctx.redo_mouseover() {
self.tooltip = None;
if let Some(ID::Road(r)) = app.mouseover_unzoomed_roads_and_intersections(ctx) {
let cost = self.costs.get(&r).cloned().unwrap_or(-1.0);
self.tooltip = Some(Text::from(format!("Cost: {}", cost)));
if let Some(cost) = self.costs.get(&r) {
self.tooltip = Some(Text::from(format!("Cost: {}", cost)));
} else {
self.tooltip = Some(Text::from("No cost"));
}
}
}

View File

@ -1,6 +1,6 @@
use std::collections::BTreeSet;
use geom::ArrowCap;
use geom::{ArrowCap, Duration};
use map_gui::render::{DrawOptions, BIG_ARROW_THICKNESS};
use map_gui::tools::PopupMsg;
use map_gui::ID;
@ -152,7 +152,7 @@ impl UberTurnViewer {
for i in &ic.members {
batch.push(Color::BLUE.alpha(0.5), map.get_i(*i).polygon.clone());
}
let mut sum_cost = 0.0;
let mut sum_cost = Duration::ZERO;
if !ic.uber_turns.is_empty() {
let ut = &ic.uber_turns[idx];
batch.push(

View File

@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
use petgraph::graphmap::DiGraphMap;
use geom::{Distance, Duration, Speed};
use geom::Duration;
pub use self::walking::{all_walking_costs_from, WalkingOptions};
use crate::pathfind::{build_graph_for_vehicles, zone_cost};
@ -77,9 +77,6 @@ pub fn all_vehicle_costs_from(
}
}
// TODO Copied from simulation code :(
let max_bike_speed = Speed::miles_per_hour(10.0);
if let Some(start_lane) = bldg_to_lane.get(&start) {
let graph = build_graph_for_vehicles(map, constraints);
let cost_per_lane = petgraph::algo::dijkstra(&graph, *start_lane, None, |(_, _, turn)| {
@ -92,9 +89,7 @@ pub fn all_vehicle_costs_from(
)
});
for (b, lane) in bldg_to_lane {
if let Some(meters) = cost_per_lane.get(&lane) {
let distance = Distance::meters(*meters as f64);
let duration = distance / max_bike_speed;
if let Some(duration) = cost_per_lane.get(&lane).cloned() {
if duration <= time_limit {
results.insert(b, duration);
}
@ -106,7 +101,10 @@ pub fn all_vehicle_costs_from(
}
// TODO Refactor with all_vehicle_costs_from
pub fn debug_vehicle_costs(req: PathRequest, map: &Map) -> Option<(f64, HashMap<RoadID, f64>)> {
pub fn debug_vehicle_costs(
req: PathRequest,
map: &Map,
) -> Option<(Duration, HashMap<RoadID, Duration>)> {
// TODO Support this
if req.constraints == PathConstraints::Pedestrian {
return None;
@ -127,7 +125,7 @@ pub fn debug_vehicle_costs(req: PathRequest, map: &Map) -> Option<(f64, HashMap<
map,
) + zone_cost(turn, req.constraints, map)
},
|_| 0.0,
|_| Duration::ZERO,
)?;
let lane_costs = petgraph::algo::dijkstra(&graph, req.start.lane(), None, |(_, _, t)| {
@ -145,7 +143,7 @@ pub fn debug_vehicle_costs(req: PathRequest, map: &Map) -> Option<(f64, HashMap<
let mut road_costs = HashMap::new();
for (l, cost) in lane_costs {
let road_cost = road_costs.entry(map.get_l(l).parent).or_insert(cost);
*road_cost = road_cost.min(cost);
*road_cost = (*road_cost).min(cost);
}
Some((cost, road_costs))

View File

@ -4,6 +4,7 @@
use serde::{Deserialize, Serialize};
use abstutil::Timer;
use geom::Duration;
use crate::pathfind::vehicles::VehiclePathfinder;
use crate::pathfind::walking::{SidewalkPathfinder, WalkingNode};
@ -108,3 +109,8 @@ impl ContractionHierarchyPathfinder {
timer.stop("apply edits to pedestrian using transit pathfinding");
}
}
pub fn round(cost: Duration) -> usize {
// Round up! 0 cost edges are ignored
(cost.inner_seconds().round() as usize).max(1)
}

View File

@ -4,6 +4,8 @@ use std::collections::BTreeSet;
use petgraph::graphmap::DiGraphMap;
use geom::Duration;
use crate::pathfind::vehicles::vehicle_cost;
use crate::pathfind::walking::{walking_cost, WalkingNode};
use crate::pathfind::zone_cost;
@ -64,7 +66,7 @@ fn calc_path(
vehicle_cost(map.get_l(turn.id.src), turn, req.constraints, params, map)
+ zone_cost(turn, req.constraints, map)
},
|_| 0.0,
|_| Duration::ZERO,
)?;
let mut steps = Vec::new();
@ -82,8 +84,8 @@ fn calc_path(
Some(Path::new(map, steps, req.clone(), Vec::new()))
}
pub fn build_graph_for_pedestrians(map: &Map) -> DiGraphMap<WalkingNode, usize> {
let mut graph: DiGraphMap<WalkingNode, usize> = DiGraphMap::new();
pub fn build_graph_for_pedestrians(map: &Map) -> DiGraphMap<WalkingNode, Duration> {
let mut graph: DiGraphMap<WalkingNode, Duration> = DiGraphMap::new();
for l in map.all_lanes() {
if l.is_walkable() {
let cost = walking_cost(l.length());
@ -100,7 +102,7 @@ pub fn build_graph_for_pedestrians(map: &Map) -> DiGraphMap<WalkingNode, usize>
map.get_l(turn.id.dst).dst_i == turn.id.parent,
),
walking_cost(turn.geom.length())
+ zone_cost(turn, PathConstraints::Pedestrian, map) as usize,
+ zone_cost(turn, PathConstraints::Pedestrian, map),
);
}
}
@ -118,7 +120,7 @@ pub fn simple_walking_path(req: &PathRequest, map: &Map) -> Option<Vec<WalkingNo
closest_start,
|end| end == closest_end,
|(_, _, cost)| *cost,
|_| 0,
|_| Duration::ZERO,
)?;
Some(path)
}

View File

@ -659,7 +659,7 @@ fn validate_zones(map: &Map, steps: &Vec<PathStep>, req: &PathRequest) {
}
/// Heavily penalize crossing into an access-restricted zone that doesn't allow this mode.
pub fn zone_cost(turn: &Turn, constraints: PathConstraints, map: &Map) -> f64 {
pub fn zone_cost(turn: &Turn, constraints: PathConstraints, map: &Map) -> Duration {
// Detect when we cross into a new zone that doesn't allow constraints.
if map
.get_parent(turn.id.src)
@ -672,11 +672,12 @@ pub fn zone_cost(turn: &Turn, constraints: PathConstraints, map: &Map) -> f64 {
.allow_through_traffic
.contains(constraints)
{
// TODO Tune this after making vehicles_cost and walking_cost both roughly represent
// seconds. In the meantime, this penalty seems high enough to achieve the desired effect.
100_000.0
// This should be high enough to achieve the desired effect of somebody not entering
// the zone unless absolutely necessary. Someone would violate that and cut through anyway
// only when the alternative route would take more than 3 hours longer!
Duration::hours(3)
} else {
0.0
Duration::ZERO
}
}
@ -685,9 +686,10 @@ pub fn zone_cost(turn: &Turn, constraints: PathConstraints, map: &Map) -> f64 {
// space-expensive change right now.
#[derive(PartialEq, Serialize, Deserialize)]
pub struct RoutingParams {
// For all vehicles
pub unprotected_turn_penalty: f64,
// For bike routing
// For all vehicles. This is added to the cost of a movement as an additional delay.
pub unprotected_turn_penalty: Duration,
// For bike routing. Multiplied by the base cost, since spending more time on the wrong lane
// type matters.
pub bike_lane_penalty: f64,
pub bus_lane_penalty: f64,
pub driving_lane_penalty: f64,
@ -696,7 +698,9 @@ pub struct RoutingParams {
impl RoutingParams {
pub const fn default() -> RoutingParams {
RoutingParams {
unprotected_turn_penalty: 2.0,
// This is a total guess -- it really depends on the traffic patterns of the particular
// road at the time we're routing.
unprotected_turn_penalty: Duration::const_seconds(30.0),
bike_lane_penalty: 1.0,
bus_lane_penalty: 1.1,
driving_lane_penalty: 1.5,

View File

@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize};
use thread_local::ThreadLocal;
use abstutil::MultiMap;
use geom::{Duration, Speed};
use crate::pathfind::ch::round;
use crate::pathfind::node_map::{deserialize_nodemap, NodeMap};
use crate::pathfind::uber_turns::{IntersectionCluster, UberTurn};
use crate::pathfind::zone_cost;
@ -187,7 +189,7 @@ fn make_input_graph(
any = true;
let ut = &uber_turns[*idx];
let mut sum_cost = 0.0;
let mut sum_cost = Duration::ZERO;
for t in &ut.path {
let turn = map.get_t(*t);
sum_cost += vehicle_cost(
@ -234,32 +236,36 @@ fn make_input_graph(
input_graph
}
/// Different unit based on constraints.
/// This returns the pathfinding cost of crossing one lane and turn. This is also expressed in
/// units of time. It factors in the ideal time to cross the space, along with penalties for
/// entering an access-restricted zone, taking an unprotected turn, and so on.
pub fn vehicle_cost(
lane: &Lane,
turn: &Turn,
constraints: PathConstraints,
params: &RoutingParams,
map: &Map,
) -> f64 {
// TODO Could cost turns differently.
) -> Duration {
let base = match constraints {
PathConstraints::Car | PathConstraints::Train => {
// Prefer slightly longer route on faster roads
let t1 = lane.length() / map.get_r(lane.parent).speed_limit;
let t2 = turn.geom.length() / map.get_parent(turn.id.dst).speed_limit;
(t1 + t2).inner_seconds()
t1 + t2
}
PathConstraints::Bike => {
// Speed limits don't matter, bikes are usually constrained by their own speed limit.
let dist = lane.length() + turn.geom.length();
// TODO Copied from sim. Probably move to map_model.
let max_bike_speed = Speed::miles_per_hour(10.0);
// Usually the bike's speed limit matters, not the road's.
let t1 = lane.length() / map.get_r(lane.parent).speed_limit.min(max_bike_speed);
let t2 =
turn.geom.length() / map.get_parent(turn.id.dst).speed_limit.min(max_bike_speed);
// TODO Elevation gain is bad, loss is good.
// TODO If we're on a driving lane, higher speed limit is worse.
// TODO Bike lanes next to parking is dangerous.
// TODO Prefer bike lanes, then bus lanes, then driving lanes. For now, express that as
// an extra cost.
// TODO Prefer bike lanes, then bus lanes, then driving lanes. For now, express that by
// multiplying the base cost.
let lt_penalty = if lane.is_biking() {
params.bike_lane_penalty
} else if lane.is_bus() {
@ -269,8 +275,7 @@ pub fn vehicle_cost(
params.driving_lane_penalty
};
// 1m resolution is fine
(lt_penalty * dist).inner_meters()
lt_penalty * (t1 + t2)
}
PathConstraints::Bus => {
// Like Car, but prefer bus lanes.
@ -282,7 +287,7 @@ pub fn vehicle_cost(
assert!(lane.is_driving());
1.1
};
(lt_penalty * (t1 + t2)).inner_seconds()
lt_penalty * (t1 + t2)
}
PathConstraints::Pedestrian => unreachable!(),
};
@ -299,7 +304,7 @@ pub fn vehicle_cost(
&& rank_from < rank_to
&& map.get_i(turn.id.parent).is_stop_sign()
{
base * params.unprotected_turn_penalty
base + params.unprotected_turn_penalty
} else {
base
};
@ -313,11 +318,8 @@ pub fn vehicle_cost(
if constraints == PathConstraints::Bike {
extra_penalty = slow_lane;
}
// TODO These are small integers, just treat them as seconds for now to micro-adjust the
// specific choice of lane.
base + (extra_penalty as f64)
}
// Round up! 0 cost edges are ignored
fn round(cost: f64) -> usize {
(cost.round() as usize).max(1)
base + Duration::seconds(extra_penalty as f64)
}

View File

@ -8,8 +8,9 @@ use fast_paths::{deserialize_32, serialize_32, FastGraph, InputGraph, PathCalcul
use serde::{Deserialize, Serialize};
use thread_local::ThreadLocal;
use geom::{Distance, Speed};
use geom::{Distance, Duration, Speed};
use crate::pathfind::ch::round;
use crate::pathfind::node_map::{deserialize_nodemap, NodeMap};
use crate::pathfind::vehicles::VehiclePathfinder;
use crate::pathfind::zone_cost;
@ -237,12 +238,12 @@ fn make_input_graph(
let mut cost = walking_cost(l.length());
// TODO Tune this penalty, along with many others.
if l.is_shoulder() {
cost *= 2;
cost = 2.0 * cost;
}
let n1 = nodes.get(WalkingNode::SidewalkEndpoint(l.id, true));
let n2 = nodes.get(WalkingNode::SidewalkEndpoint(l.id, false));
input_graph.add_edge(n1, n2, cost);
input_graph.add_edge(n2, n1, cost);
input_graph.add_edge(n1, n2, round(cost));
input_graph.add_edge(n2, n1, round(cost));
}
}
@ -255,8 +256,9 @@ fn make_input_graph(
input_graph.add_edge(
nodes.get(from),
nodes.get(to),
walking_cost(t.geom.length())
+ zone_cost(t, PathConstraints::Pedestrian, map) as usize,
round(
walking_cost(t.geom.length()) + zone_cost(t, PathConstraints::Pedestrian, map),
),
);
}
}
@ -286,12 +288,12 @@ fn transit_input_graph(
} else {
walking_cost(stop.sidewalk_pos.dist_along())
};
// Add some extra penalty (equivalent to 1m) to using a bus stop. Otherwise a path
// might try to pass through it uselessly.
let penalty = 100;
// Add some extra penalty to using a bus stop. Otherwise a path might try to pass
// through it uselessly.
let penalty = Duration::seconds(1.0);
let sidewalk = nodes.get(WalkingNode::SidewalkEndpoint(lane.id, *endpt));
input_graph.add_edge(sidewalk, ride_bus, cost + penalty);
input_graph.add_edge(ride_bus, sidewalk, cost + penalty);
input_graph.add_edge(sidewalk, ride_bus, round(cost + penalty));
input_graph.add_edge(ride_bus, sidewalk, round(cost + penalty));
}
}
@ -380,12 +382,10 @@ fn transit_input_graph(
}
}
/// The cost is time in seconds, rounded to a usize
// TODO Plumb RoutingParams here, but first, need to also plumb in the turn or lane.
pub fn walking_cost(dist: Distance) -> usize {
pub fn walking_cost(dist: Distance) -> Duration {
let walking_speed = Speed::meters_per_second(1.34);
let time = dist / walking_speed;
(time.inner_seconds().round() as usize).max(1)
dist / walking_speed
}
pub fn walking_path_to_steps(path: Vec<WalkingNode>, map: &Map) -> Vec<PathStep> {