Refactor a concept of per-trip Problem. #600, #170

The simulation analytics tracks problems per trip. The previous
measurements for delay at intersections is one case, and the new
cyclist-crossing-large-interection event is another. I removed the lane
speed measurement, since it's kind of redundant with the delay.

Consolidated the reporting in the trip info panel / drawn on the route.
This commit is contained in:
Dustin Carlino 2021-04-17 09:48:16 -07:00
parent 6f5a7b980a
commit 6380bd74ec
6 changed files with 129 additions and 224 deletions

View File

@ -5,7 +5,10 @@ use maplit::btreemap;
use geom::{Distance, Duration, Percent, Polygon, Pt2D};
use map_gui::ID;
use map_model::{Map, Path, PathStep};
use sim::{AgentID, PersonID, TripEndpoint, TripID, TripMode, TripPhase, TripPhaseType};
use sim::{
AgentID, Analytics, PersonID, Problem, TripEndpoint, TripID, TripInfo, TripMode, TripPhase,
TripPhaseType,
};
use widgetry::{
Color, ControlState, DrawWithTooltips, EventCtx, GeomBatch, Line, LinePlot, PlotOptions,
RewriteColor, Series, Text, TextExt, Widget,
@ -115,30 +118,13 @@ pub fn ongoing(
]),
]));
}
if trip.mode != TripMode::Drive {
// TODO Terminology?
col.push(Widget::custom_row(vec![
Line("Risks")
.secondary()
.into_widget(ctx)
.container()
.force_width_pct(ctx, col_width),
Text::from_all(vec![
Line(
app.primary
.sim
.get_analytics()
.large_intersection_crossings
.get(&id)
.map(|x| x.len())
.unwrap_or(0)
.to_string(),
),
Line(" crossings at large intersections").secondary(),
])
.into_widget(ctx),
]));
}
col.push(describe_problems(
ctx,
app.primary.sim.get_analytics(),
id,
&trip,
col_width,
));
{
col.push(Widget::custom_row(vec![
Widget::custom_row(vec![Line("Purpose").secondary().into_widget(ctx)])
@ -355,33 +341,17 @@ pub fn finished(
Line(trip.purpose.to_string()).secondary().into_widget(ctx),
]));
}
// TODO Duplicating some code from ongoing() until we decide how to consolidate things.
if trip.mode != TripMode::Drive {
let analytics = if open_trips[&id].show_after {
col.push(describe_problems(
ctx,
if open_trips[&id].show_after {
app.primary.sim.get_analytics()
} else {
app.prebaked()
};
col.push(Widget::custom_row(vec![
Line("Risks")
.secondary()
.into_widget(ctx)
.container()
.force_width_pct(ctx, col_width),
Text::from_all(vec![
Line(
analytics
.large_intersection_crossings
.get(&id)
.map(|x| x.len())
.unwrap_or(0)
.to_string(),
),
Line(" crossings at large intersections").secondary(),
])
.into_widget(ctx),
]));
}
},
id,
&trip,
col_width,
));
col.push(make_trip_details(
ctx,
@ -428,94 +398,82 @@ pub fn cancelled(
Widget::col(col)
}
/// Highlights intersections which were "slow" on the map
fn highlight_slow_intersections(ctx: &EventCtx, app: &App, details: &mut Details, id: TripID) {
if let Some(delays) = app
.primary
.sim
.get_analytics()
.trip_intersection_delays
.get(&id)
{
for (id, time) in delays {
let intersection = app.primary.map.get_i(id.parent);
let (normal_delay_time, slow_delay_time) = if intersection.is_traffic_signal() {
(30, 120)
} else {
(5, 30)
};
let (fg_color, bg_color) = if *time < normal_delay_time {
(Color::WHITE, app.cs.normal_slow_intersection)
} else if *time < slow_delay_time {
(Color::BLACK, app.cs.slow_intersection)
} else {
(Color::WHITE, app.cs.very_slow_intersection)
};
let duration = Duration::seconds(*time as f64);
details.unzoomed.append(
Text::from(Line(format!("{}", duration)).fg(fg_color))
.bg(bg_color)
.render(ctx)
.centered_on(intersection.polygon.center()),
);
details.zoomed.append(
Text::from(Line(format!("{}", duration)).fg(fg_color))
.bg(bg_color)
.render(ctx)
.scale(0.4)
.centered_on(intersection.polygon.center()),
);
fn describe_problems(
ctx: &mut EventCtx,
analytics: &Analytics,
id: TripID,
trip: &TripInfo,
col_width: Percent,
) -> Widget {
if trip.mode == TripMode::Bike {
let mut count = 0;
let empty = Vec::new();
for problem in analytics.problems_per_trip.get(&id).unwrap_or(&empty) {
if let Problem::LargeIntersectionCrossing(_) = problem {
count += 1;
}
}
Widget::custom_row(vec![
Line("Risk exposure")
.secondary()
.into_widget(ctx)
.container()
.force_width_pct(ctx, col_width),
Text::from_all(vec![
Line(count.to_string()),
// TODO Singular/plural
Line(" crossings at large intersections").secondary(),
])
.into_widget(ctx),
])
} else {
Widget::nothing()
}
}
/// Highlights lanes which were "slow" on the map
fn highlight_slow_lanes(ctx: &EventCtx, app: &App, details: &mut Details, id: TripID) {
if let Some(lane_speeds) = app
fn draw_problems(ctx: &EventCtx, app: &App, details: &mut Details, id: TripID) {
let empty = Vec::new();
for problem in app
.primary
.sim
.get_analytics()
.lane_speed_percentage
.problems_per_trip
.get(&id)
.unwrap_or(&empty)
{
for (id, speed_percent) in lane_speeds.iter() {
let lane = app.primary.map.get_l(*id);
let (fg_color, bg_color) = if speed_percent > &95 {
(Color::WHITE, app.cs.normal_slow_intersection)
} else if speed_percent > &60 {
(Color::BLACK, app.cs.slow_intersection)
} else {
(Color::WHITE, app.cs.very_slow_intersection)
};
details.unzoomed.push(
bg_color,
lane.lane_center_pts.make_polygons(Distance::meters(10.0)),
);
details.zoomed.extend(
bg_color,
lane.lane_center_pts.dashed_lines(
Distance::meters(0.75),
Distance::meters(1.0),
Distance::meters(0.4),
),
);
let (pt, _) = lane
.lane_center_pts
.must_dist_along(lane.lane_center_pts.length() / 2.0);
details.unzoomed.append(
Text::from(Line(format!("{}s", speed_percent)).fg(fg_color))
.bg(bg_color)
.render(ctx)
.centered_on(pt),
);
details.zoomed.append(
Text::from(Line(format!("{}s", speed_percent)).fg(fg_color))
.bg(bg_color)
.render(ctx)
.scale(0.4)
.centered_on(pt),
);
match problem {
Problem::IntersectionDelay(i, delay) => {
let i = app.primary.map.get_i(*i);
// TODO These thresholds don't match what we use as thresholds in the simulation.
let (slow, slower) = if i.is_traffic_signal() {
(Duration::seconds(30.0), Duration::minutes(2))
} else {
(Duration::seconds(5.0), Duration::seconds(30.0))
};
let (fg_color, bg_color) = if *delay < slow {
(Color::WHITE, app.cs.slow_intersection)
} else if *delay < slower {
(Color::BLACK, app.cs.slower_intersection)
} else {
(Color::WHITE, app.cs.slowest_intersection)
};
details.unzoomed.append(
Text::from(Line(format!("{}", delay)).fg(fg_color))
.bg(bg_color)
.render(ctx)
.centered_on(i.polygon.center()),
);
details.zoomed.append(
Text::from(Line(format!("{}", delay)).fg(fg_color))
.bg(bg_color)
.render(ctx)
.scale(0.4)
.centered_on(i.polygon.center()),
);
}
Problem::LargeIntersectionCrossing(_) => {
// TODO Maybe a caution icon?
}
}
}
}
@ -802,8 +760,7 @@ fn make_trip_details(
col.push("Map edits have disconnected the path taken before".text_widget(ctx));
}
col.extend(elevation);
highlight_slow_intersections(ctx, app, details, trip_id);
highlight_slow_lanes(ctx, app, details, trip_id);
draw_problems(ctx, app, details, trip_id);
Widget::col(col)
}

View File

@ -99,10 +99,10 @@ pub struct ColorScheme {
pub signal_spinner: Color,
pub signal_turn_block_bg: Color,
// Timeline delay highlighting
pub very_slow_intersection: Color,
// Problems encountered on a trip
pub slowest_intersection: Color,
pub slower_intersection: Color,
pub slow_intersection: Color,
pub normal_slow_intersection: Color,
// Other static elements
pub void_background: Color,
@ -233,10 +233,10 @@ impl ColorScheme {
signal_spinner: hex("#F2994A"),
signal_turn_block_bg: Color::grey(0.6),
// Timeline delay highlighting
very_slow_intersection: Color::RED,
slow_intersection: Color::YELLOW,
normal_slow_intersection: Color::GREEN,
// Problems encountered on a trip
slowest_intersection: Color::RED,
slower_intersection: Color::YELLOW,
slow_intersection: Color::GREEN,
// Other static elements
void_background: Color::BLACK,

View File

@ -6,12 +6,11 @@ use abstutil::Counter;
use geom::{Duration, Time};
use map_model::{
BusRouteID, BusStopID, CompressedMovementID, IntersectionID, LaneID, Map, MovementID,
ParkingLotID, Path, PathRequest, RoadID, Traversable, TurnID, TurnType,
ParkingLotID, Path, PathRequest, RoadID, Traversable, TurnType,
};
use crate::{
AgentID, AgentType, AlertLocation, CarID, Event, ParkingSpot, TripID, TripMode, TripPhaseType,
VehicleType,
};
/// As a simulation runs, different pieces emit Events. The Analytics object listens to these,
@ -45,17 +44,8 @@ pub struct Analytics {
/// Finish time, ID, mode, trip duration if successful (or None if cancelled)
pub finished_trips: Vec<(Time, TripID, TripMode, Option<Duration>)>,
/// Records how long was spent waiting at each turn (Intersection) for a given trip
/// Over a certain threshold
/// TripID, [(TurnID, Time Waiting In Seconds)]
pub trip_intersection_delays: BTreeMap<TripID, BTreeMap<TurnID, u8>>,
/// Records the average speed/maximum speed for each lane
/// If it is over a certain threshold (<95% of max speed)
/// TripID, [(LaneID, Percent of maximum speed as an integer (0-100)]
pub lane_speed_percentage: BTreeMap<TripID, BTreeMap<LaneID, u8>>,
/// Record every instance of somebody on foot or a bike crossing an intersection with more than
/// 4 connecting roads.
pub large_intersection_crossings: BTreeMap<TripID, Vec<(Time, IntersectionID)>>,
/// Record different problems that each trip encounters.
pub problems_per_trip: BTreeMap<TripID, Vec<Problem>>,
// TODO This subsumes finished_trips
pub trip_log: Vec<(Time, TripID, Option<PathRequest>, TripPhaseType)>,
@ -74,6 +64,14 @@ pub struct Analytics {
record_anything: bool,
}
#[derive(Clone, Serialize, Deserialize)]
pub enum Problem {
/// A vehicle waited >30s, or a pedestrian waited >15s.
IntersectionDelay(IntersectionID, Duration),
/// A cyclist crossed an intersection with >4 connecting roads.
LargeIntersectionCrossing(IntersectionID),
}
impl Analytics {
pub fn new(record_anything: bool) -> Analytics {
Analytics {
@ -86,9 +84,7 @@ impl Analytics {
passengers_alighting: BTreeMap::new(),
started_trips: BTreeMap::new(),
finished_trips: Vec::new(),
trip_intersection_delays: BTreeMap::new(),
lane_speed_percentage: BTreeMap::new(),
large_intersection_crossings: BTreeMap::new(),
problems_per_trip: BTreeMap::new(),
trip_log: Vec::new(),
intersection_delays: BTreeMap::new(),
parking_lane_changes: BTreeMap::new(),
@ -197,24 +193,17 @@ impl Analytics {
// Intersection delay
if let Event::IntersectionDelayMeasured(trip_id, turn_id, agent, delay) = ev {
match agent {
AgentID::Car(_) => {
if delay > Duration::seconds(30.0) {
self.trip_intersection_delays
.entry(trip_id)
.or_insert_with(BTreeMap::new)
.insert(turn_id, delay.inner_seconds() as u8);
}
}
AgentID::Pedestrian(_) => {
if delay > Duration::seconds(15.0) {
self.trip_intersection_delays
.entry(trip_id)
.or_insert_with(BTreeMap::new)
.insert(turn_id, delay.inner_seconds() as u8);
}
}
AgentID::BusPassenger(_, _) => {}
let threshold = match agent {
AgentID::Car(_) => Duration::seconds(30.0),
AgentID::Pedestrian(_) => Duration::seconds(15.0),
// Don't record for riders
AgentID::BusPassenger(_, _) => Duration::hours(24),
};
if delay > threshold {
self.problems_per_trip
.entry(trip_id)
.or_insert_with(Vec::new)
.push(Problem::IntersectionDelay(turn_id.parent, delay));
}
// SharedSidewalkCorner are always no-conflict, immediate turns; they're not
@ -230,17 +219,6 @@ impl Analytics {
}
}
// Lane Speed
if let Event::LaneSpeedPercentage(trip_id, lane_id, avg_speed, max_speed) = ev {
let speed_percent: u8 = ((avg_speed / max_speed) * 100.0) as u8;
if speed_percent < 95 {
self.lane_speed_percentage
.entry(trip_id)
.or_insert_with(BTreeMap::new)
.insert(lane_id, speed_percent);
}
}
// Parking spot changes
if let Event::CarReachedParkingSpot(_, spot) = ev {
if let ParkingSpot::Onstreet(l, _) = spot {
@ -271,26 +249,14 @@ impl Analytics {
// Safety metrics
if let Event::AgentEntersTraversable(a, Some(trip), Traversable::Turn(t), _) = ev {
if match a {
AgentID::Pedestrian(_) => true,
AgentID::Car(c) => c.1 == VehicleType::Bike,
_ => false,
} {
//
// - If a pedestrian never enters a crosswalk at a large intersection, don't record.
// - Note one intersection will get counted multiple times if a pedestrian uses
// several crosswalks there.
// - Defining a "large intersection" is tricky. If a road is split into two
// one-ways, should we count it as two roads? If we haven't consolidated some
// crazy intersection, we won't see it.
if map.get_t(t).turn_type != TurnType::SharedSidewalkCorner
&& map.get_i(t.parent).roads.len() > 4
{
self.large_intersection_crossings
.entry(trip)
.or_insert_with(Vec::new)
.push((time, t.parent));
}
if a.to_type() == AgentType::Bike && map.get_i(t.parent).roads.len() > 4 {
// Defining a "large intersection" is tricky. If a road is split into two one-ways,
// should we count it as two roads? If we haven't consolidated some crazy
// intersection, we won't see it.
self.problems_per_trip
.entry(trip)
.or_insert_with(Vec::new)
.push(Problem::LargeIntersectionCrossing(t.parent));
}
}

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use geom::{Duration, Speed};
use geom::Duration;
use map_model::{
BuildingID, BusRouteID, BusStopID, IntersectionID, LaneID, Map, Path, PathRequest, Traversable,
TurnID,
@ -51,8 +51,6 @@ pub enum Event {
},
TripCancelled(TripID, TripMode),
TripPhaseStarting(TripID, PersonID, Option<PathRequest>, TripPhaseType),
/// TripID, LaneID (Where the delay was encountered), Average Speed, Max Speed
LaneSpeedPercentage(TripID, LaneID, Speed, Speed),
/// Just use for parking replanning. Not happy about copying the full path in here, but the way
/// to plumb info into Analytics is Event.

View File

@ -30,7 +30,7 @@ pub use crate::render::{
UnzoomedAgent,
};
pub use self::analytics::{Analytics, TripPhase};
pub use self::analytics::{Analytics, Problem, TripPhase};
pub(crate) use self::cap::CapSimState;
pub(crate) use self::events::Event;
pub use self::events::{AlertLocation, TripPhaseType};

View File

@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use serde::{Deserialize, Serialize};
use abstutil::{deserialize_hashmap, serialize_hashmap, FixedMap, IndexableKey};
use geom::{Distance, Duration, PolyLine, Speed, Time};
use geom::{Distance, Duration, PolyLine, Time};
use map_model::{IntersectionID, LaneID, Map, Path, Position, Traversable};
use crate::mechanics::car::{Car, CarState};
@ -272,23 +272,7 @@ impl DrivingSimState {
transit: &mut TransitSimState,
) -> bool {
match car.state {
CarState::Crossing(time_int, dist_int) => {
if let Some((trip, _)) = car.trip_and_person {
if let Traversable::Lane(lane) = car.router.head() {
let time_to_cross = now - time_int.start;
if time_to_cross > Duration::ZERO {
let avg_speed = Speed::from_dist_time(dist_int.length(), time_to_cross);
let max_speed = car.router.head().max_speed_along(
car.vehicle.max_speed,
car.vehicle.vehicle_type.to_constraints(),
ctx.map,
);
self.events
.push(Event::LaneSpeedPercentage(trip, lane, avg_speed, max_speed));
}
}
}
CarState::Crossing(_, _) => {
car.state = CarState::Queued { blocked_since: now };
if car.router.last_step() {
// Immediately run update_car_with_distances.