Plumb in the tuned routing params. When they're not the defaults, automatically fallback from CH to Dijkstra's. #494

This commit is contained in:
Dustin Carlino 2021-02-02 15:20:06 -08:00
parent eebe7a098a
commit 32bcfbe738
12 changed files with 165 additions and 82 deletions

View File

@ -49,7 +49,11 @@ impl ObjectDebugger {
let mut costs = Vec::new();
for turn in map.get_turns_to_lane(l.id) {
costs.push(map_model::connectivity::driving_cost(
l, turn, constraint, map,
l,
turn,
constraint,
map.routing_params(),
map,
));
}
println!("Costs for {:?}: {:?}", constraint, costs);

View File

@ -1,5 +1,5 @@
use map_gui::ID;
use map_model::NORMAL_LANE_THICKNESS;
use map_model::{RoutingParams, NORMAL_LANE_THICKNESS};
use sim::{TripEndpoint, TripMode};
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Line, Outcome, Panel,
@ -26,39 +26,35 @@ impl RouteExplorer {
Line("Route explorer").small_heading().draw(ctx),
ctx.style().btn_close_widget(ctx),
]),
profile_to_controls(ctx, &RoutingProfile::default_biking()).named("profile"),
params_to_controls(ctx, TripMode::Bike, &RoutingParams::default()).named("params"),
]))
.aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
.build(ctx),
})
}
fn controls_to_profile(&self) -> RoutingProfile {
fn controls_to_params(&self) -> (TripMode, RoutingParams) {
let mut params = RoutingParams::default();
if !self.panel.is_button_enabled("cars") {
return RoutingProfile::Driving;
return (TripMode::Drive, params);
}
if !self.panel.is_button_enabled("pedestrians") {
return RoutingProfile::Walking;
}
RoutingProfile::Biking {
bike_lane_penalty: self.panel.spinner("bike lane penalty") as f64 / 10.0,
bus_lane_penalty: self.panel.spinner("bus lane penalty") as f64 / 10.0,
driving_lane_penalty: self.panel.spinner("driving lane penalty") as f64 / 10.0,
return (TripMode::Walk, params);
}
params.bike_lane_penalty = self.panel.spinner("bike lane penalty") as f64 / 10.0;
params.bus_lane_penalty = self.panel.spinner("bus lane penalty") as f64 / 10.0;
params.driving_lane_penalty = self.panel.spinner("driving lane penalty") as f64 / 10.0;
(TripMode::Bike, params)
}
fn recalc_paths(&mut self, ctx: &mut EventCtx, app: &App) {
let mode = match self.controls_to_profile() {
RoutingProfile::Driving => TripMode::Drive,
RoutingProfile::Walking => TripMode::Walk,
RoutingProfile::Biking { .. } => TripMode::Bike,
};
let (mode, params) = self.controls_to_params();
if let Some((ref goal, _, ref mut preview)) = self.goal {
*preview = Drawable::empty(ctx);
if let Some(polygon) =
TripEndpoint::path_req(self.start.clone(), goal.clone(), mode, &app.primary.map)
.and_then(|req| app.primary.map.pathfind(req).ok())
.and_then(|req| app.primary.map.pathfind_with_params(req, &params).ok())
.and_then(|path| path.trace(&app.primary.map))
.map(|pl| pl.make_polygons(NORMAL_LANE_THICKNESS))
{
@ -78,18 +74,21 @@ impl State<App> for RouteExplorer {
return Transition::Pop;
}
"bikes" => {
let controls = profile_to_controls(ctx, &RoutingProfile::default_biking());
self.panel.replace(ctx, "profile", controls);
let controls =
params_to_controls(ctx, TripMode::Bike, &RoutingParams::default());
self.panel.replace(ctx, "params", controls);
self.recalc_paths(ctx, app);
}
"cars" => {
let controls = profile_to_controls(ctx, &RoutingProfile::Driving);
self.panel.replace(ctx, "profile", controls);
let controls =
params_to_controls(ctx, TripMode::Drive, &RoutingParams::default());
self.panel.replace(ctx, "params", controls);
self.recalc_paths(ctx, app);
}
"pedestrians" => {
let controls = profile_to_controls(ctx, &RoutingProfile::Walking);
self.panel.replace(ctx, "profile", controls);
let controls =
params_to_controls(ctx, TripMode::Walk, &RoutingParams::default());
self.panel.replace(ctx, "params", controls);
self.recalc_paths(ctx, app);
}
_ => unreachable!(),
@ -178,65 +177,37 @@ impl State<App> for RouteExplorer {
}
}
// TODO Move to map_model
// TODO Not sure an enum makes sense, based on how we're still going to be toggling based on
// PathConstraints.
enum RoutingProfile {
Driving,
Biking {
bike_lane_penalty: f64,
bus_lane_penalty: f64,
driving_lane_penalty: f64,
},
Walking,
}
impl RoutingProfile {
fn default_biking() -> RoutingProfile {
RoutingProfile::Biking {
bike_lane_penalty: 1.0,
bus_lane_penalty: 1.1,
driving_lane_penalty: 1.5,
}
}
}
fn profile_to_controls(ctx: &mut EventCtx, profile: &RoutingProfile) -> Widget {
fn params_to_controls(ctx: &mut EventCtx, mode: TripMode, params: &RoutingParams) -> Widget {
let mut rows = vec![Widget::custom_row(vec![
ctx.style()
.btn_plain_light_icon("system/assets/meters/bike.svg")
.disabled(matches!(profile, RoutingProfile::Biking { .. }))
.disabled(mode == TripMode::Bike)
.build_widget(ctx, "bikes"),
ctx.style()
.btn_plain_light_icon("system/assets/meters/car.svg")
.disabled(matches!(profile, RoutingProfile::Driving))
.disabled(mode == TripMode::Drive)
.build_widget(ctx, "cars"),
ctx.style()
.btn_plain_light_icon("system/assets/meters/pedestrian.svg")
.disabled(matches!(profile, RoutingProfile::Walking))
.disabled(mode == TripMode::Walk)
.build_widget(ctx, "pedestrians"),
])
.evenly_spaced()];
if let RoutingProfile::Biking {
bike_lane_penalty,
bus_lane_penalty,
driving_lane_penalty,
} = profile
{
if mode == TripMode::Bike {
// TODO Spinners that natively understand a floating point range with a given precision
rows.push(Widget::row(vec![
"Bike lane penalty:".draw_text(ctx).margin_right(20),
Spinner::new(ctx, (0, 20), (*bike_lane_penalty * 10.0) as isize)
Spinner::new(ctx, (0, 20), (params.bike_lane_penalty * 10.0) as isize)
.named("bike lane penalty"),
]));
rows.push(Widget::row(vec![
"Bus lane penalty:".draw_text(ctx).margin_right(20),
Spinner::new(ctx, (0, 20), (*bus_lane_penalty * 10.0) as isize)
Spinner::new(ctx, (0, 20), (params.bus_lane_penalty * 10.0) as isize)
.named("bus lane penalty"),
]));
rows.push(Widget::row(vec![
"Driving lane penalty:".draw_text(ctx).margin_right(20),
Spinner::new(ctx, (0, 20), (*driving_lane_penalty * 10.0) as isize)
Spinner::new(ctx, (0, 20), (params.driving_lane_penalty * 10.0) as isize)
.named("driving lane penalty"),
]));
}

View File

@ -174,6 +174,7 @@ impl UberTurnViewer {
map.get_l(t.src),
map.get_t(*t),
PathConstraints::Car,
map.routing_params(),
map,
);
}

View File

@ -83,7 +83,13 @@ pub fn all_vehicle_costs_from(
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)| {
driving_cost(map.get_l(turn.src), map.get_t(*turn), constraints, map)
driving_cost(
map.get_l(turn.src),
map.get_t(*turn),
constraints,
map.routing_params(),
map,
)
});
for (b, lane) in bldg_to_lane {
if let Some(meters) = cost_per_lane.get(&lane) {

View File

@ -56,7 +56,7 @@ pub use crate::objects::turn::{
pub use crate::objects::zone::{AccessRestrictions, Zone};
pub use crate::pathfind::uber_turns::{IntersectionCluster, UberTurn, UberTurnGroup};
use crate::pathfind::Pathfinder;
pub use crate::pathfind::{Path, PathConstraints, PathRequest, PathStep};
pub use crate::pathfind::{Path, PathConstraints, PathRequest, PathStep, RoutingParams};
pub use crate::traversable::{Position, Traversable};
mod city;

View File

@ -15,7 +15,8 @@ use crate::{
osm, Area, AreaID, AreaType, Building, BuildingID, BuildingType, BusRoute, BusRouteID, BusStop,
BusStopID, ControlStopSign, ControlTrafficSignal, Intersection, IntersectionID, Lane, LaneID,
LaneType, Map, MapEdits, MovementID, OffstreetParking, ParkingLot, ParkingLotID, Path,
PathConstraints, PathRequest, Pathfinder, Position, Road, RoadID, Turn, TurnID, TurnType, Zone,
PathConstraints, PathRequest, Pathfinder, Position, Road, RoadID, RoutingParams, Turn, TurnID,
TurnType, Zone,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -562,6 +563,12 @@ impl Map {
assert!(!self.pathfinder_dirty);
self.pathfinder.pathfind_avoiding_lanes(req, avoid, self)
}
pub fn pathfind_with_params(&self, req: PathRequest, params: &RoutingParams) -> Result<Path> {
assert!(!self.pathfinder_dirty);
self.pathfinder
.pathfind_with_params(req.clone(), params, self)
.ok_or_else(|| anyhow!("can't fulfill {}", req))
}
pub fn should_use_transit(
&self,
@ -703,4 +710,13 @@ impl Map {
.collect(),
)
}
/// Returns the routing params baked into the map. Currently just hardcoded defaults.
// Depending how this works out, we might require everybody to explicitly plumb routing params,
// in which case it should be easy to look for all places calling this.
pub fn routing_params(&self) -> &'static RoutingParams {
&ROUTING_PARAMS
}
}
static ROUTING_PARAMS: RoutingParams = RoutingParams::default();

View File

@ -86,7 +86,13 @@ impl Zone {
req.start.lane(),
|l| l == req.end.lane(),
|(_, _, turn)| {
driving_cost(map.get_l(turn.src), map.get_t(*turn), req.constraints, map)
driving_cost(
map.get_l(turn.src),
map.get_t(*turn),
req.constraints,
map.routing_params(),
map,
)
},
|_| 0.0,
)?;

View File

@ -6,13 +6,13 @@ use petgraph::graphmap::DiGraphMap;
use crate::pathfind::driving::driving_cost;
use crate::pathfind::walking::{walking_cost, WalkingNode};
use crate::{LaneID, Map, Path, PathConstraints, PathRequest, PathStep, TurnID};
use crate::{LaneID, Map, Path, PathConstraints, PathRequest, PathStep, RoutingParams, TurnID};
// TODO These should maybe keep the DiGraphMaps as state. It's cheap to recalculate it for edits.
pub fn simple_pathfind(req: &PathRequest, map: &Map) -> Option<Path> {
pub fn simple_pathfind(req: &PathRequest, params: &RoutingParams, map: &Map) -> Option<Path> {
let graph = build_graph_for_vehicles(map, req.constraints);
calc_path(graph, req, map)
calc_path(graph, req, params, map)
}
pub fn build_graph_for_vehicles(
@ -45,15 +45,28 @@ pub fn pathfind_avoiding_lanes(
}
}
calc_path(graph, &req, map)
calc_path(graph, &req, map.routing_params(), map)
}
fn calc_path(graph: DiGraphMap<LaneID, TurnID>, req: &PathRequest, map: &Map) -> Option<Path> {
fn calc_path(
graph: DiGraphMap<LaneID, TurnID>,
req: &PathRequest,
params: &RoutingParams,
map: &Map,
) -> Option<Path> {
let (_, path) = petgraph::algo::astar(
&graph,
req.start.lane(),
|l| l == req.end.lane(),
|(_, _, turn)| driving_cost(map.get_l(turn.src), map.get_t(*turn), req.constraints, map),
|(_, _, turn)| {
driving_cost(
map.get_l(turn.src),
map.get_t(*turn),
req.constraints,
params,
map,
)
},
|_| 0.0,
)?;
let mut steps = Vec::new();

View File

@ -10,7 +10,9 @@ use abstutil::MultiMap;
use crate::pathfind::node_map::{deserialize_nodemap, NodeMap};
use crate::pathfind::uber_turns::{IntersectionCluster, UberTurn};
use crate::{Lane, LaneID, Map, Path, PathConstraints, PathRequest, PathStep, Turn, TurnID};
use crate::{
Lane, LaneID, Map, Path, PathConstraints, PathRequest, PathStep, RoutingParams, Turn, TurnID,
};
#[derive(Serialize, Deserialize)]
pub struct VehiclePathfinder {
@ -183,7 +185,13 @@ fn make_input_graph(
input_graph.add_edge(
from,
nodes.get(Node::Lane(turn.id.dst)),
round(driving_cost(l, turn, constraints, map)),
round(driving_cost(
l,
turn,
constraints,
map.routing_params(),
map,
)),
);
}
} else {
@ -193,7 +201,13 @@ fn make_input_graph(
let mut sum_cost = 0.0;
for t in &ut.path {
sum_cost += driving_cost(map.get_l(t.src), map.get_t(*t), constraints, map);
sum_cost += driving_cost(
map.get_l(t.src),
map.get_t(*t),
constraints,
map.routing_params(),
map,
);
}
input_graph.add_edge(from, nodes.get(Node::UberTurn(*idx)), round(sum_cost));
input_graph.add_edge(
@ -219,7 +233,13 @@ fn make_input_graph(
}
/// Different unit based on constraints.
pub fn driving_cost(lane: &Lane, turn: &Turn, constraints: PathConstraints, map: &Map) -> f64 {
pub fn driving_cost(
lane: &Lane,
turn: &Turn,
constraints: PathConstraints,
params: &RoutingParams,
map: &Map,
) -> f64 {
// TODO Could cost turns differently.
let base = match constraints {
@ -239,12 +259,12 @@ pub fn driving_cost(lane: &Lane, turn: &Turn, constraints: PathConstraints, map:
// TODO Prefer bike lanes, then bus lanes, then driving lanes. For now, express that as
// an extra cost.
let lt_penalty = if lane.is_biking() {
1.0
params.bike_lane_penalty
} else if lane.is_bus() {
1.1
params.bus_lane_penalty
} else {
assert!(lane.is_driving());
1.5
params.driving_lane_penalty
};
// 1m resolution is fine

View File

@ -639,3 +639,24 @@ fn validate_restrictions(map: &Map, steps: &Vec<PathStep>) {
}
}
}
/// Tuneable parameters for all types of routing.
// These will maybe become part of the PathRequest later, but that's an extremely invasive and
// space-expensive change right now.
#[derive(PartialEq)]
pub struct RoutingParams {
// For bike routing
pub bike_lane_penalty: f64,
pub bus_lane_penalty: f64,
pub driving_lane_penalty: f64,
}
impl RoutingParams {
pub const fn default() -> RoutingParams {
RoutingParams {
bike_lane_penalty: 1.0,
bus_lane_penalty: 1.1,
driving_lane_penalty: 1.5,
}
}
}

View File

@ -9,7 +9,7 @@ use crate::pathfind::walking::{one_step_walking_path, walking_path_to_steps};
use crate::pathfind::{dijkstra, WalkingNode};
use crate::{
BusRouteID, BusStopID, Intersection, LaneID, Map, Path, PathConstraints, PathRequest, Position,
TurnID, Zone,
RoutingParams, TurnID, Zone,
};
/// Most of the time, prefer using the faster contraction hierarchies. But sometimes, callers can
@ -25,6 +25,17 @@ impl Pathfinder {
/// Finds a path from a start to an end for a certain type of agent. Handles requests that
/// start or end inside access-restricted zones.
pub fn pathfind(&self, req: PathRequest, map: &Map) -> Option<Path> {
self.pathfind_with_params(req, map.routing_params(), map)
}
/// Finds a path from a start to an end for a certain type of agent. Handles requests that
/// start or end inside access-restricted zones. May use custom routing parameters.
pub fn pathfind_with_params(
&self,
req: PathRequest,
params: &RoutingParams,
map: &Map,
) -> Option<Path> {
if req.start.lane() == req.end.lane() && req.constraints == PathConstraints::Pedestrian {
return Some(one_step_walking_path(&req, map));
}
@ -104,7 +115,7 @@ impl Pathfinder {
let steps = walking_path_to_steps(self.simple_walking_path(&req, map)?, map);
return Some(Path::new(map, steps, req, Vec::new()));
}
self.simple_pathfind(&req, map)
self.simple_pathfind(&req, params, map)
}
pub fn pathfind_avoiding_lanes(
@ -138,9 +149,22 @@ impl Pathfinder {
}
// Doesn't handle zones or pedestrians
fn simple_pathfind(&self, req: &PathRequest, map: &Map) -> Option<Path> {
fn simple_pathfind(
&self,
req: &PathRequest,
params: &RoutingParams,
map: &Map,
) -> Option<Path> {
if params != &RoutingParams::default() {
// If the params differ from the defaults, the CHs won't match. This should only be
// happening from the debug UI; be very obnoxious if we start calling it from the
// simulation or something else.
warn!("Pathfinding for {} with custom params", req);
return dijkstra::simple_pathfind(req, params, map);
}
match self {
Pathfinder::Dijkstra => dijkstra::simple_pathfind(req, map),
Pathfinder::Dijkstra => dijkstra::simple_pathfind(req, params, map),
Pathfinder::CH(ref p) => p.simple_pathfind(req, map),
}
}
@ -225,7 +249,7 @@ impl Pathfinder {
}
let mut interior_path = zone.pathfind(interior_req, map)?;
let main_path = self.simple_pathfind(&req, map)?;
let main_path = self.simple_pathfind(&req, map.routing_params(), map)?;
interior_path.append(main_path, map);
Some(interior_path)
}
@ -305,7 +329,7 @@ impl Pathfinder {
}
let interior_path = zone.pathfind(interior_req, map)?;
let mut main_path = self.simple_pathfind(&req, map)?;
let mut main_path = self.simple_pathfind(&req, map.routing_params(), map)?;
main_path.append(interior_path, map);
main_path.orig_req = orig_req;
Some(main_path)

View File

@ -385,6 +385,7 @@ 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 {
let walking_speed = Speed::meters_per_second(1.34);
let time = dist / walking_speed;