From 1a10e8982a909d5407fc9cdc4944c1c56b986c68 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Fri, 4 Oct 2019 15:33:26 -0700 Subject: [PATCH] basic heatmap showing busiest roads and intersections. have to revive Sim Events kind of. --- abstutil/src/collections.rs | 6 + docs/TODO_logistic.md | 1 - docs/TODO_quality.md | 9 +- docs/TODO_ux.md | 8 -- game/src/common/colors.rs | 16 ++- game/src/sandbox/mod.rs | 28 ++++- game/src/sandbox/thruput_stats.rs | 123 +++++++++++++++++++ game/src/sandbox/{stats.rs => trip_stats.rs} | 13 +- sim/src/events.rs | 1 - sim/src/mechanics/driving.rs | 12 +- sim/src/mechanics/walking.rs | 15 ++- sim/src/sim.rs | 27 ++-- sim/src/trips.rs | 2 +- 13 files changed, 212 insertions(+), 49 deletions(-) create mode 100644 game/src/sandbox/thruput_stats.rs rename game/src/sandbox/{stats.rs => trip_stats.rs} (98%) diff --git a/abstutil/src/collections.rs b/abstutil/src/collections.rs index 8363926d14..da544d78ce 100644 --- a/abstutil/src/collections.rs +++ b/abstutil/src/collections.rs @@ -75,6 +75,12 @@ impl Counter { pub fn get(&self, val: T) -> usize { self.map.get(&val).cloned().unwrap_or(0) } + + pub fn sorted_asc(&self) -> Vec<&T> { + let mut list = self.map.iter().collect::>(); + list.sort_by_key(|(_, cnt)| *cnt); + list.into_iter().map(|(t, _)| t).collect() + } } pub fn wraparound_get(vec: &Vec, idx: isize) -> &T { diff --git a/docs/TODO_logistic.md b/docs/TODO_logistic.md index 1268afe200..ba9e22daf5 100644 --- a/docs/TODO_logistic.md +++ b/docs/TODO_logistic.md @@ -3,7 +3,6 @@ - enable more clippy lints - enforce consistent style (derive order, struct initialization order) -- update with mission statement (democratized urb p, that quote, refashion existing space cheaply) - trailer - show common parts of routes in A/B, point of divergence - "Two parallel universes sit at your fingertips, and with the flick of a key, you can glide between the two. Buses jumping past traffic in one world, snarly traffic jam in the other. An A/B test revealing what currently is, and what could be, compared meticulously and deterministically. A/B Street -- which world do you prefer?" diff --git a/docs/TODO_quality.md b/docs/TODO_quality.md index 842825a477..031c5ef2ef 100644 --- a/docs/TODO_quality.md +++ b/docs/TODO_quality.md @@ -22,9 +22,6 @@ - deal with loop roads? - model U-turns -- degenerate-2's should only have one crosswalk - - then make them thinner - - car turns often clip sidewalk corners now - draw SharedSidewalkCorners just around the ped path, not arbitrarily thick - dont forget to draw the notches @@ -84,10 +81,6 @@ - just revert intersection and warn - or store overrides more granularly and warn or do something reasonable -## Release - -- publish the map data - ## Sim bugs/tests needed - do bikes use bike lanes? @@ -95,11 +88,11 @@ - make sure that we can jump to a ped on a bus and see the bus - park/unpark needs to jump two lanes in the case of crossing a bike lane or something - should only be able to park from the closest lane, though! +- explicit tests making cars park at 0 and max_dist, peds walk to 0 and max_dist ## Discrete-event sim model - cleanup after the cutover - - explicit tests making cars park at 0 and max_dist, peds walk to 0 and max_dist - proper intersection policies, by seeing full view - time travel mode can be very smart - dupe code for initially spawning vs spawning when a trip leg starts. diff --git a/docs/TODO_ux.md b/docs/TODO_ux.md index cb1c17e06d..5708568bfa 100644 --- a/docs/TODO_ux.md +++ b/docs/TODO_ux.md @@ -2,9 +2,6 @@ ## Fix existing stuff -- try showing traffic signals by little boxes at the end of lanes - - red circle means right turn on red OK, red right arrow means nope, green means normal turns ok, green arrow means protected left, crosswalk hand or stick figure - - if a lane could feasibly have multiple turn options but doesnt, print "ONLY" - audit all panics - tune text color, size, padding @@ -15,8 +12,6 @@ - yellow or flashing red/yellow for yields - text box entry: highlight char looks like replace mode; draw it btwn chars -- traffic signal cycles go offscreen sometimes! - - navigator - show options on map - stop jumping text size @@ -27,7 +22,6 @@ - when dragging, dont give mouse movement to UI elements - start context menu when left click releases and we're not dragging - can we change labels in modal or top menu? show/hide -- label sections of modal menus - distinguish hints from status of modal menus, for hiding purposes - move context menus out of ezgui - simplify/remove UserInput. @@ -43,8 +37,6 @@ ## Better rendering - depict residential bldg occupany size somehow -- render overlapping peds reasonably -- draw moving / blocked colors (gradually more red as they wait longer) - render cars with textures? - rooftops - https://thumbs.dreamstime.com/b/top-view-city-street-asphalt-transport-people-walking-down-sidewalk-intersecting-road-pedestrian-81034411.jpg diff --git a/game/src/common/colors.rs b/game/src/common/colors.rs index 6bc8a048a3..4fb24ba983 100644 --- a/game/src/common/colors.rs +++ b/game/src/common/colors.rs @@ -76,6 +76,7 @@ impl RoadColorerBuilder { pub struct ObjectColorerBuilder { zoomed_override_colors: HashMap, legend: ColorLegend, + roads: Vec<(RoadID, Color)>, } pub struct ObjectColorer { @@ -104,14 +105,19 @@ impl ObjectColorerBuilder { ObjectColorerBuilder { zoomed_override_colors: HashMap::new(), legend: ColorLegend::new(title, rows), + roads: Vec::new(), } } pub fn add(&mut self, id: ID, color: Color) { - self.zoomed_override_colors.insert(id, color); + if let ID::Road(r) = id { + self.roads.push((r, color)); + } else { + self.zoomed_override_colors.insert(id, color); + } } - pub fn build(self, ctx: &mut EventCtx, map: &Map) -> ObjectColorer { + pub fn build(mut self, ctx: &mut EventCtx, map: &Map) -> ObjectColorer { let mut batch = GeomBatch::new(); for (id, color) in &self.zoomed_override_colors { let poly = match id { @@ -121,6 +127,12 @@ impl ObjectColorerBuilder { }; batch.push(*color, poly); } + for (r, color) in self.roads { + batch.push(color, map.get_r(r).get_thick_polygon().unwrap()); + for l in map.get_r(r).all_lanes() { + self.zoomed_override_colors.insert(ID::Lane(l), color); + } + } ObjectColorer { zoomed_override_colors: self.zoomed_override_colors, unzoomed: ctx.prerender.upload(batch), diff --git a/game/src/sandbox/mod.rs b/game/src/sandbox/mod.rs index 9afad45ad4..c408bcc9f6 100644 --- a/game/src/sandbox/mod.rs +++ b/game/src/sandbox/mod.rs @@ -1,7 +1,8 @@ mod score; mod spawner; -mod stats; +mod thruput_stats; mod time_travel; +mod trip_stats; use crate::common::{ time_controls, AgentTools, CommonState, ObjectColorer, ObjectColorerBuilder, RoadColorer, @@ -25,7 +26,8 @@ pub struct SandboxMode { speed: SpeedControls, agent_tools: AgentTools, pub time_travel: time_travel::InactiveTimeTravel, - stats: stats::TripStats, + trip_stats: trip_stats::TripStats, + thruput_stats: thruput_stats::ThruputStats, common: CommonState, parking_heatmap: Option<(Duration, RoadColorer)>, intersection_delay_heatmap: Option<(Duration, ObjectColorer)>, @@ -38,7 +40,10 @@ impl SandboxMode { speed: SpeedControls::new(ctx, None), agent_tools: AgentTools::new(ctx), time_travel: time_travel::InactiveTimeTravel::new(), - stats: stats::TripStats::new(ui.primary.current_flags.sim_flags.opts.record_stats), + trip_stats: trip_stats::TripStats::new( + ui.primary.current_flags.sim_flags.opts.record_stats, + ), + thruput_stats: thruput_stats::ThruputStats::new(), common: CommonState::new(), parking_heatmap: None, intersection_delay_heatmap: None, @@ -68,6 +73,7 @@ impl SandboxMode { (hotkey(Key::T), "start time traveling"), (hotkey(Key::Q), "scoreboard"), (None, "trip stats"), + (None, "throughput stats"), ], vec![ (hotkey(Key::Escape), "quit"), @@ -88,7 +94,8 @@ impl SandboxMode { impl State for SandboxMode { fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition { self.time_travel.record(ui); - self.stats.record(ui); + self.trip_stats.record(ui); + self.thruput_stats.record(ui); { let mut txt = Text::prompt("Sandbox Mode"); @@ -124,7 +131,18 @@ impl State for SandboxMode { return Transition::Push(Box::new(score::Scoreboard::new(ctx, ui))); } if self.menu.action("trip stats") { - return Transition::Push(Box::new(stats::ShowStats::new(&self.stats, ui, ctx))); + if let Some(s) = trip_stats::ShowStats::new(&self.trip_stats, ui, ctx) { + return Transition::Push(Box::new(s)); + } else { + println!("No trip stats available"); + } + } + if self.menu.action("throughput stats") { + return Transition::Push(Box::new(thruput_stats::ShowStats::new( + &self.thruput_stats, + ui, + ctx, + ))); } if self.menu.action("show/hide parking availability") { if self.parking_heatmap.is_some() { diff --git a/game/src/sandbox/thruput_stats.rs b/game/src/sandbox/thruput_stats.rs new file mode 100644 index 0000000000..0e7a348b2a --- /dev/null +++ b/game/src/sandbox/thruput_stats.rs @@ -0,0 +1,123 @@ +use crate::common::{ObjectColorer, ObjectColorerBuilder}; +use crate::game::{State, Transition}; +use crate::helpers::ID; +use crate::ui::UI; +use abstutil::Counter; +use ezgui::{hotkey, Color, EventCtx, GfxCtx, Key, ModalMenu}; +use map_model::{IntersectionID, RoadID, Traversable}; +use sim::Event; + +pub struct ThruputStats { + count_per_road: Counter, + count_per_intersection: Counter, +} + +impl ThruputStats { + pub fn new() -> ThruputStats { + ThruputStats { + count_per_road: Counter::new(), + count_per_intersection: Counter::new(), + } + } + + pub fn record(&mut self, ui: &mut UI) { + for ev in ui.primary.sim.collect_events() { + if let Event::AgentEntersTraversable(_, to) = ev { + match to { + Traversable::Lane(l) => self.count_per_road.inc(ui.primary.map.get_l(l).parent), + Traversable::Turn(t) => self.count_per_intersection.inc(t.parent), + }; + } + } + } +} + +pub struct ShowStats { + menu: ModalMenu, + heatmap: ObjectColorer, +} + +impl State for ShowStats { + fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition { + ctx.canvas.handle_event(ctx.input); + if ctx.redo_mouseover() { + ui.recalculate_current_selection(ctx); + } + + self.menu.handle_event(ctx, None); + if self.menu.action("quit") { + return Transition::Pop; + } + Transition::Keep + } + + fn draw_default_ui(&self) -> bool { + false + } + + fn draw(&self, g: &mut GfxCtx, ui: &UI) { + self.heatmap.draw(g, ui); + self.menu.draw(g); + } +} + +impl ShowStats { + pub fn new(stats: &ThruputStats, ui: &UI, ctx: &mut EventCtx) -> ShowStats { + let light = Color::GREEN; + let medium = Color::YELLOW; + let heavy = Color::RED; + let mut colorer = ObjectColorerBuilder::new( + "Throughput", + vec![ + ("< 50%ile", light), + ("< 90%ile", medium), + (">= 90%ile", heavy), + ], + ); + + // TODO If there are many duplicate counts, arbitrarily some will look heavier! Find the + // disribution of counts instead. + // TODO Actually display the counts at these percentiles + // TODO Dump the data in debug mode + { + let roads = stats.count_per_road.sorted_asc(); + let p50_idx = ((roads.len() as f64) * 0.5) as usize; + let p90_idx = ((roads.len() as f64) * 0.9) as usize; + for (idx, r) in roads.into_iter().enumerate() { + let color = if idx < p50_idx { + light + } else if idx < p90_idx { + medium + } else { + heavy + }; + colorer.add(ID::Road(*r), color); + } + } + // TODO dedupe + { + let intersections = stats.count_per_intersection.sorted_asc(); + let p50_idx = ((intersections.len() as f64) * 0.5) as usize; + let p90_idx = ((intersections.len() as f64) * 0.9) as usize; + for (idx, i) in intersections.into_iter().enumerate() { + let color = if idx < p50_idx { + light + } else if idx < p90_idx { + medium + } else { + heavy + }; + colorer.add(ID::Intersection(*i), color); + } + } + + ShowStats { + menu: ModalMenu::new( + "Thruput Stats", + vec![vec![(hotkey(Key::Escape), "quit")]], + ctx, + ), + heatmap: colorer.build(ctx, &ui.primary.map), + } + } +} diff --git a/game/src/sandbox/stats.rs b/game/src/sandbox/trip_stats.rs similarity index 98% rename from game/src/sandbox/stats.rs rename to game/src/sandbox/trip_stats.rs index 7103edf0c4..ed8214ee20 100644 --- a/game/src/sandbox/stats.rs +++ b/game/src/sandbox/trip_stats.rs @@ -104,7 +104,11 @@ impl State for ShowStats { } impl ShowStats { - pub fn new(stats: &TripStats, ui: &UI, ctx: &mut EventCtx) -> ShowStats { + pub fn new(stats: &TripStats, ui: &UI, ctx: &mut EventCtx) -> Option { + if stats.samples.is_empty() { + return None; + } + let mut batch = GeomBatch::new(); let mut labels = MultiText::new(); @@ -184,9 +188,6 @@ impl ShowStats { } for (_, color, getter) in lines { - if stats.samples.is_empty() { - continue; - } let mut pts = Vec::new(); if max_y == 0 { pts.push(Pt2D::new(x1, y2)); @@ -217,12 +218,12 @@ impl ShowStats { "{} samples", abstutil::prettyprint_usize(stats.samples.len()) ))); - ShowStats { + Some(ShowStats { menu: ModalMenu::new("Trip Stats", vec![vec![(hotkey(Key::Escape), "quit")]], ctx) .set_prompt(ctx, txt), draw: ctx.prerender.upload(batch), labels, legend, - } + }) } } diff --git a/sim/src/events.rs b/sim/src/events.rs index 4d8b1f8651..1592b0df6a 100644 --- a/sim/src/events.rs +++ b/sim/src/events.rs @@ -19,6 +19,5 @@ pub enum Event { BikeStoppedAtSidewalk(CarID, LaneID), - // TODO Remove this one AgentEntersTraversable(AgentID, Traversable), } diff --git a/sim/src/mechanics/driving.rs b/sim/src/mechanics/driving.rs index 8d37ae6390..0722527b03 100644 --- a/sim/src/mechanics/driving.rs +++ b/sim/src/mechanics/driving.rs @@ -1,7 +1,7 @@ use crate::mechanics::car::{Car, CarState}; use crate::mechanics::queue::Queue; use crate::{ - ActionAtEnd, AgentID, CarID, Command, CreateCar, DistanceInterval, DrawCarInput, + ActionAtEnd, AgentID, CarID, Command, CreateCar, DistanceInterval, DrawCarInput, Event, IntersectionSimState, ParkedCar, ParkingSimState, Scheduler, TimeInterval, TransitSimState, TripManager, TripPositions, UnzoomedAgent, WalkingSimState, FOLLOWING_DISTANCE, }; @@ -32,6 +32,7 @@ pub struct DrivingSimState { deserialize_with = "deserialize_btreemap" )] queues: BTreeMap, + events: Vec, } impl DrivingSimState { @@ -39,6 +40,7 @@ impl DrivingSimState { let mut sim = DrivingSimState { cars: BTreeMap::new(), queues: BTreeMap::new(), + events: Vec::new(), }; for l in map.all_lanes() { @@ -342,6 +344,10 @@ impl DrivingSimState { car.state = car.crossing_state(Distance::ZERO, now, map); car.blocked_since = None; scheduler.push(car.state.get_end_time(), Command::UpdateCar(car.vehicle.id)); + self.events.push(Event::AgentEntersTraversable( + AgentID::Car(car.vehicle.id), + goto, + )); car.last_steps.push_front(last_step); if goto.length(map) >= car.vehicle.length + FOLLOWING_DISTANCE { @@ -899,4 +905,8 @@ impl DrivingSimState { } false } + + pub fn collect_events(&mut self) -> Vec { + std::mem::replace(&mut self.events, Vec::new()) + } } diff --git a/sim/src/mechanics/walking.rs b/sim/src/mechanics/walking.rs index 9ae73c5e4e..6e5b74c9ed 100644 --- a/sim/src/mechanics/walking.rs +++ b/sim/src/mechanics/walking.rs @@ -1,6 +1,6 @@ use crate::{ AgentID, AgentMetadata, Command, CreatePedestrian, DistanceInterval, DrawPedCrowdInput, - DrawPedestrianInput, IntersectionSimState, ParkingSimState, ParkingSpot, PedestrianID, + DrawPedestrianInput, Event, IntersectionSimState, ParkingSimState, ParkingSpot, PedestrianID, Scheduler, SidewalkPOI, SidewalkSpot, TimeInterval, TransitSimState, TripID, TripManager, TripPositions, UnzoomedAgent, }; @@ -22,6 +22,7 @@ pub struct WalkingSimState { deserialize_with = "deserialize_multimap" )] peds_per_traversable: MultiMap, + events: Vec, } impl WalkingSimState { @@ -29,6 +30,7 @@ impl WalkingSimState { WalkingSimState { peds: BTreeMap::new(), peds_per_traversable: MultiMap::new(), + events: Vec::new(), } } @@ -164,6 +166,7 @@ impl WalkingSimState { map, intersections, &mut self.peds_per_traversable, + &mut self.events, scheduler, ) { scheduler.push(ped.state.get_end_time(), Command::UpdatePed(ped.id)); @@ -180,6 +183,7 @@ impl WalkingSimState { map, intersections, &mut self.peds_per_traversable, + &mut self.events, scheduler, ) { scheduler.push(ped.state.get_end_time(), Command::UpdatePed(ped.id)); @@ -377,6 +381,10 @@ impl WalkingSimState { (loners, crowds) } + + pub fn collect_events(&mut self) -> Vec { + std::mem::replace(&mut self.events, Vec::new()) + } } #[derive(Serialize, Deserialize, PartialEq)] @@ -529,6 +537,7 @@ impl Pedestrian { map: &Map, intersections: &mut IntersectionSimState, peds_per_traversable: &mut MultiMap, + events: &mut Vec, scheduler: &mut Scheduler, ) -> bool { if let PathStep::Turn(t) = self.path.next_step() { @@ -554,6 +563,10 @@ impl Pedestrian { }; self.state = self.crossing_state(start_dist, now, map); peds_per_traversable.insert(self.path.current_step().as_traversable(), self.id); + events.push(Event::AgentEntersTraversable( + AgentID::Pedestrian(self.id), + self.path.current_step().as_traversable(), + )); true } } diff --git a/sim/src/sim.rs b/sim/src/sim.rs index 4db80bfb9c..c858c0a6e6 100644 --- a/sim/src/sim.rs +++ b/sim/src/sim.rs @@ -50,10 +50,7 @@ pub struct Sim { #[derivative(PartialEq = "ignore")] #[serde(skip_serializing, skip_deserializing)] trip_positions: Option, - - #[derivative(PartialEq = "ignore")] - #[serde(skip_serializing, skip_deserializing)] - events_since_last_step: Vec, + // TODO Maybe the buffered events in child objects should also have this. } #[derive(Clone)] @@ -112,7 +109,6 @@ impl Sim { run_name: opts.run_name, step_count: 0, trip_positions: None, - events_since_last_step: Vec::new(), } } @@ -518,12 +514,6 @@ impl Sim { self.time = target_time; self.trip_positions = None; - - self.events_since_last_step.clear(); - self.events_since_last_step - .extend(self.trips.collect_events()); - self.events_since_last_step - .extend(self.transit.collect_events()); } pub fn timed_step(&mut self, map: &Map, dt: Duration, timer: &mut Timer) { @@ -646,8 +636,8 @@ impl Sim { let mut expectations = VecDeque::from(all_expectations); self.step(&map, self.time() + time_limit); - for ev in self.get_events_since_last_step() { - if ev == expectations.front().unwrap() { + for ev in self.collect_events() { + if &ev == expectations.front().unwrap() { println!("At {}, met expectation {:?}", self.time, ev); expectations.pop_front(); if expectations.is_empty() { @@ -864,8 +854,15 @@ impl Sim { self.trip_positions.as_ref().unwrap() } - pub fn get_events_since_last_step(&self) -> &Vec { - &self.events_since_last_step + // This only supports one caller! And the result isn't time-sorted. + // TODO If nobody calls this, slow sad memory leak. Push style would probably be much nicer. + pub fn collect_events(&mut self) -> Vec { + let mut events = Vec::new(); + events.extend(self.trips.collect_events()); + events.extend(self.transit.collect_events()); + events.extend(self.driving.collect_events()); + events.extend(self.walking.collect_events()); + events } pub fn get_canonical_pt_per_trip(&self, trip: TripID, map: &Map) -> TripResult { diff --git a/sim/src/trips.rs b/sim/src/trips.rs index 64af195786..478237999a 100644 --- a/sim/src/trips.rs +++ b/sim/src/trips.rs @@ -483,7 +483,7 @@ impl TripManager { } pub fn collect_events(&mut self) -> Vec { - self.events.drain(..).collect() + std::mem::replace(&mut self.events, Vec::new()) } pub fn trip_status(&self, id: TripID) -> TripStatus {