overhaul finished trips graph. record times in order, build the graph later

This commit is contained in:
Dustin Carlino 2019-10-31 13:46:26 -07:00
parent c89c0d2a11
commit ef01501509
9 changed files with 156 additions and 189 deletions

View File

@ -176,7 +176,6 @@ fn launch_test(test: &ABTest, ui: &mut UI, ctx: &mut EventCtx) -> ABTestMode {
.sim_flags
.opts
.disable_block_the_box,
record_stats: false,
recalc_lanechanging: current_flags
.sim_flags
.opts

View File

@ -1,4 +1,4 @@
use super::trip_stats::{ShowTripStats, TripStats};
use super::trip_stats::ShowTripStats;
use crate::common::{ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder};
use crate::game::{Transition, WizardState};
use crate::helpers::ID;
@ -31,7 +31,6 @@ impl Analytics {
ctx: &mut EventCtx,
ui: &UI,
menu: &mut MenuUnderButton,
trip_stats: &TripStats,
) -> Option<Transition> {
if menu.action("change analytics overlay") {
return Some(Transition::Push(WizardState::new(Box::new(
@ -51,8 +50,7 @@ impl Analytics {
})?;
Some(Transition::PopWithData(Box::new(move |state, ui, ctx| {
let mut sandbox = state.downcast_mut::<SandboxMode>().unwrap();
sandbox.analytics =
Analytics::recalc(&choice, &sandbox.trip_stats, ui, ctx);
sandbox.analytics = Analytics::recalc(&choice, ui, ctx);
})))
},
))));
@ -74,7 +72,7 @@ impl Analytics {
}
};
if time != ui.primary.sim.time() {
*self = Analytics::recalc(choice, trip_stats, ui, ctx);
*self = Analytics::recalc(choice, ui, ctx);
}
None
}
@ -111,7 +109,7 @@ impl Analytics {
}
}
fn recalc(choice: &str, trip_stats: &TripStats, ui: &UI, ctx: &mut EventCtx) -> Analytics {
fn recalc(choice: &str, ui: &UI, ctx: &mut EventCtx) -> Analytics {
let time = ui.primary.sim.time();
match choice {
"none" => Analytics::Inactive,
@ -123,10 +121,10 @@ impl Analytics {
}
"cumulative throughput" => Analytics::Throughput(time, calculate_thruput(ctx, ui)),
"finished trips" => {
if let Some(s) = ShowTripStats::new(trip_stats, ui, ctx) {
if let Some(s) = ShowTripStats::new(ui, ctx) {
Analytics::FinishedTrips(time, s)
} else {
println!("No trip stats available. Pass --record_stats or make sure at least one trip is done.");
println!("No data on finished trips yet");
Analytics::Inactive
}
}

View File

@ -25,7 +25,6 @@ pub struct SandboxMode {
general_tools: MenuUnderButton,
save_tools: MenuUnderButton,
agent_tools: AgentTools,
trip_stats: trip_stats::TripStats,
analytics: analytics::Analytics,
gameplay: gameplay::GameplayState,
common: CommonState,
@ -73,9 +72,6 @@ impl SandboxMode {
ctx,
),
agent_tools: AgentTools::new(),
trip_stats: trip_stats::TripStats::new(
ui.primary.current_flags.sim_flags.opts.record_stats,
),
analytics: analytics::Analytics::Inactive,
gameplay: gameplay::GameplayState::initialize(mode, ui, ctx),
common: CommonState::new(ctx),
@ -87,8 +83,6 @@ impl SandboxMode {
impl State for SandboxMode {
fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition {
self.trip_stats.record(ui);
layout::stack_vertically(
layout::ContainerOrientation::TopRight,
ctx.canvas,
@ -115,10 +109,7 @@ impl State for SandboxMode {
if let Some(t) = self.common.event(ctx, ui) {
return t;
}
if let Some(t) = self
.analytics
.event(ctx, ui, &mut self.info_tools, &self.trip_stats)
{
if let Some(t) = self.analytics.event(ctx, ui, &mut self.info_tools) {
return t;
}
if let Some(t) = self.gameplay.event(ctx, ui, &mut self.analytics) {

View File

@ -1,79 +1,14 @@
use crate::common::ColorLegend;
use crate::ui::UI;
use abstutil::Counter;
use ezgui::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, MultiText, ScreenPt, ScreenRectangle, Text,
};
use geom::{Distance, Duration, PolyLine, Polygon, Pt2D};
use sim::TripMode;
use std::collections::BTreeMap;
pub struct TripStats {
should_record: bool,
samples: Vec<StateAtTime>,
}
struct StateAtTime {
time: Duration,
// These're all cumulative
finished_walk_trips: usize,
finished_bike_trips: usize,
finished_transit_trips: usize,
finished_drive_trips: usize,
aborted_trips: usize,
}
impl TripStats {
pub fn new(should_record: bool) -> TripStats {
TripStats {
should_record,
samples: Vec::new(),
}
}
pub fn record(&mut self, ui: &UI) {
if !self.should_record {
return;
}
if let Some(ref state) = self.samples.last() {
// Already have this
if ui.primary.sim.time() == state.time {
return;
}
// We just loaded a new savestate or reset or something. Clear out our memory.
if ui.primary.sim.time() < state.time {
self.samples.clear();
}
}
let t = ui.primary.sim.get_finished_trips();
let mut state = StateAtTime {
time: ui.primary.sim.time(),
finished_walk_trips: 0,
finished_bike_trips: 0,
finished_transit_trips: 0,
finished_drive_trips: 0,
aborted_trips: t.aborted_trips,
};
for (_, m, _) in t.finished_trips {
match m {
TripMode::Walk => {
state.finished_walk_trips += 1;
}
TripMode::Bike => {
state.finished_bike_trips += 1;
}
TripMode::Transit => {
state.finished_transit_trips += 1;
}
TripMode::Drive => {
state.finished_drive_trips += 1;
}
}
}
self.samples.push(state);
}
}
// TODO Show active trips too
pub struct ShowTripStats {
draw: Drawable,
legend: ColorLegend,
@ -82,121 +17,61 @@ pub struct ShowTripStats {
}
impl ShowTripStats {
pub fn new(stats: &TripStats, ui: &UI, ctx: &mut EventCtx) -> Option<ShowTripStats> {
if stats.samples.is_empty() {
pub fn new(ui: &UI, ctx: &mut EventCtx) -> Option<ShowTripStats> {
if ui.primary.sim.get_analytics().finished_trips.is_empty() {
return None;
}
let mut batch = GeomBatch::new();
let mut labels = MultiText::new();
let x1 = 0.2 * ctx.canvas.window_width;
let x2 = 0.8 * ctx.canvas.window_width;
let y1 = 0.2 * ctx.canvas.window_height;
let y2 = 0.8 * ctx.canvas.window_height;
batch.push(
Color::grey(0.8),
Polygon::rectangle_topleft(
Pt2D::new(x1, y1),
Distance::meters(x2 - x1),
Distance::meters(y2 - y1),
),
);
let lines: Vec<(&str, Color, Box<dyn Fn(&StateAtTime) -> usize>)> = vec![
let lines: Vec<(&str, Color, Option<TripMode>)> = vec![
(
"walking",
ui.cs.get("unzoomed pedestrian"),
Box::new(|s| s.finished_walk_trips),
),
(
"biking",
ui.cs.get("unzoomed bike"),
Box::new(|s| s.finished_bike_trips),
Some(TripMode::Walk),
),
("biking", ui.cs.get("unzoomed bike"), Some(TripMode::Bike)),
(
"transit",
ui.cs.get("unzoomed bus"),
Box::new(|s| s.finished_transit_trips),
),
(
"driving",
ui.cs.get("unzoomed car"),
Box::new(|s| s.finished_drive_trips),
),
(
"aborted",
Color::PURPLE.alpha(0.5),
Box::new(|s| s.aborted_trips),
Some(TripMode::Transit),
),
("driving", ui.cs.get("unzoomed car"), Some(TripMode::Drive)),
("aborted", Color::PURPLE.alpha(0.5), None),
];
let legend = ColorLegend::new(
Text::prompt("finished trips"),
lines
.iter()
.map(|(name, color, _)| (*name, *color))
.collect(),
);
let max_y = stats
.samples
.iter()
.map(|s| lines.iter().map(|(_, _, getter)| getter(s)).max().unwrap())
.max()
.unwrap();
// Y-axis labels
for i in 0..=5 {
let percent = f64::from(i) / 5.0;
labels.add(
Text::from(Line(((percent * (max_y as f64)) as usize).to_string())),
ScreenPt::new(x1, y2 - percent * (y2 - y1)),
);
}
// X-axis labels (currently nonlinear!)
if stats.samples.len() > 1 {
let num_pts = stats.samples.len().min(5);
for i in 0..num_pts {
let percent_x = (i as f64) / ((num_pts - 1) as f64);
let t =
stats.samples[(percent_x * ((stats.samples.len() - 1) as f64)) as usize].time;
labels.add(
Text::from(Line(t.to_string())),
ScreenPt::new(x1 + percent_x * (x2 - x1), y2),
);
}
// What times do we use for interpolation?
let num_x_pts = 100;
let mut times = Vec::new();
for i in 0..num_x_pts {
let percent_x = (i as f64) / ((num_x_pts - 1) as f64);
let t = ui.primary.sim.time() * percent_x;
times.push(t);
}
for (_, color, getter) in lines {
let mut pts = Vec::new();
if max_y == 0 {
pts.push(Pt2D::new(x1, y2));
pts.push(Pt2D::new(x2, y2));
} else {
let num_pts = stats.samples.len().min(10);
for i in 0..num_pts {
let percent_x = (i as f64) / ((num_pts - 1) as f64);
let value = getter(
&stats.samples[(percent_x * ((stats.samples.len() - 1) as f64)) as usize],
);
let percent_y = (value as f64) / (max_y as f64);
pts.push(Pt2D::new(
x1 + (x2 - x1) * percent_x,
// Y inversion! :D
y2 - (y2 - y1) * percent_y,
));
// Gather the data
let mut counts = Counter::new();
let mut pts_per_mode: BTreeMap<Option<TripMode>, Vec<(Duration, usize)>> =
lines.iter().map(|(_, _, m)| (*m, Vec::new())).collect();
for (t, m) in &ui.primary.sim.get_analytics().finished_trips {
counts.inc(*m);
if *t > times[0] {
times.remove(0);
for (_, _, mode) in &lines {
pts_per_mode
.get_mut(mode)
.unwrap()
.push((*t, counts.get(*mode)));
}
}
batch.push(
color,
PolyLine::new(pts).make_polygons(Distance::meters(5.0)),
);
}
Some(ShowTripStats {
draw: ctx.prerender.upload(batch),
labels,
legend,
rect: ScreenRectangle { x1, y1, x2, y2 },
})
Some(plot(
"finished trips",
lines
.into_iter()
.map(|(name, color, m)| (name, color, pts_per_mode.remove(&m).unwrap()))
.collect(),
ctx,
))
}
pub fn draw(&self, g: &mut GfxCtx) {
@ -210,3 +85,89 @@ impl ShowTripStats {
g.canvas.mark_covered_area(self.rect.clone());
}
}
fn plot(
title: &str,
series: Vec<(&str, Color, Vec<(Duration, usize)>)>,
ctx: &EventCtx,
) -> ShowTripStats {
let mut batch = GeomBatch::new();
let mut labels = MultiText::new();
let x1 = 0.2 * ctx.canvas.window_width;
let x2 = 0.8 * ctx.canvas.window_width;
let y1 = 0.2 * ctx.canvas.window_height;
let y2 = 0.8 * ctx.canvas.window_height;
batch.push(
Color::grey(0.8),
Polygon::rectangle_topleft(
Pt2D::new(x1, y1),
Distance::meters(x2 - x1),
Distance::meters(y2 - y1),
),
);
// Assume every series has data at exactly the same durations
let num_x_labels = 5;
let max_x = series[0].2.last().unwrap().0;
for i in 0..num_x_labels {
let percent_x = (i as f64) / ((num_x_labels - 1) as f64);
let t = series[0].2[(percent_x * ((series[0].2.len() - 1) as f64)) as usize].0;
labels.add(
Text::from(Line(t.to_string())),
ScreenPt::new(x1 + percent_x * (x2 - x1), y2),
);
}
// Don't assume the data is cumulative
let max_y = series
.iter()
.map(|(_, _, pts)| pts.iter().map(|(_, cnt)| *cnt).max().unwrap())
.max()
.unwrap();
let num_y_labels = 5;
for i in 0..num_y_labels {
let percent_y = (i as f64) / ((num_y_labels - 1) as f64);
labels.add(
Text::from(Line(((percent_y * (max_y as f64)) as usize).to_string())),
ScreenPt::new(x1, y2 - percent_y * (y2 - y1)),
);
}
let legend = ColorLegend::new(
Text::prompt(title),
series
.iter()
.map(|(name, color, _)| (*name, *color))
.collect(),
);
for (_, color, raw_pts) in series {
let mut pts = Vec::new();
if max_y == 0 {
pts.push(Pt2D::new(x1, y2));
pts.push(Pt2D::new(x2, y2));
} else {
for (t, y) in raw_pts {
let percent_x = t / max_x;
let percent_y = (y as f64) / (max_y as f64);
pts.push(Pt2D::new(
x1 + (x2 - x1) * percent_x,
// Y inversion! :D
y2 - (y2 - y1) * percent_y,
));
}
}
batch.push(
color,
PolyLine::new(pts).make_polygons(Distance::meters(5.0)),
);
}
ShowTripStats {
draw: ctx.prerender.upload(batch),
labels,
legend,
rect: ScreenRectangle { x1, y1, x2, y2 },
}
}

View File

@ -1,4 +1,4 @@
use crate::Event;
use crate::{Event, TripMode};
use abstutil::Counter;
use geom::Duration;
use map_model::{BusRouteID, BusStopID, IntersectionID, Map, RoadID, Traversable};
@ -11,6 +11,8 @@ pub struct Analytics {
pub(crate) test_expectations: VecDeque<Event>,
pub bus_arrivals: HashMap<(BusStopID, BusRouteID), Vec<Duration>>,
pub total_bus_passengers: Counter<BusRouteID>,
// TODO Hack: No TripMode means aborted
pub finished_trips: Vec<(Duration, Option<TripMode>)>,
}
pub struct ThruputStats {
@ -28,6 +30,7 @@ impl Default for Analytics {
test_expectations: VecDeque::new(),
bus_arrivals: HashMap::new(),
total_bus_passengers: Counter::new(),
finished_trips: Vec::new(),
}
}
}
@ -60,5 +63,12 @@ impl Analytics {
if let Event::PedEntersBus(_, _, route) = ev {
self.total_bus_passengers.inc(route);
}
// Finished trips
if let Event::TripFinished(_, mode) = ev {
self.finished_trips.push((time, Some(mode)));
} else if let Event::TripAborted(_) = ev {
self.finished_trips.push((time, None));
}
}
}

View File

@ -1,4 +1,4 @@
use crate::{AgentID, CarID, ParkingSpot, PedestrianID};
use crate::{AgentID, CarID, ParkingSpot, PedestrianID, TripID, TripMode};
use map_model::{BuildingID, BusRouteID, BusStopID, IntersectionID, LaneID, Traversable};
use serde_derive::{Deserialize, Serialize};
@ -20,4 +20,7 @@ pub enum Event {
BikeStoppedAtSidewalk(CarID, LaneID),
AgentEntersTraversable(AgentID, Traversable),
TripFinished(TripID, TripMode),
TripAborted(TripID),
}

View File

@ -28,7 +28,6 @@ impl SimFlags {
savestate_every: args.optional_parse("--savestate_every", Duration::parse),
use_freeform_policy_everywhere: args.enabled("--freeform_policy"),
disable_block_the_box: args.enabled("--disable_block_the_box"),
record_stats: args.enabled("--record_stats"),
recalc_lanechanging: !args.enabled("--dont_recalc_lc"),
},
}

View File

@ -62,7 +62,6 @@ pub struct SimOptions {
pub savestate_every: Option<Duration>,
pub use_freeform_policy_everywhere: bool,
pub disable_block_the_box: bool,
pub record_stats: bool,
pub recalc_lanechanging: bool,
}
@ -73,7 +72,6 @@ impl SimOptions {
savestate_every: None,
use_freeform_policy_everywhere: false,
disable_block_the_box: false,
record_stats: false,
recalc_lanechanging: true,
}
}

View File

@ -138,6 +138,7 @@ impl TripManager {
assert!(!trip.finished_at.is_some());
trip.finished_at = Some(now);
self.unfinished_trips -= 1;
self.events.push(Event::TripFinished(trip.id, trip.mode));
return;
}
_ => {}
@ -199,6 +200,7 @@ impl TripManager {
);
self.unfinished_trips -= 1;
trip.aborted = true;
self.events.push(Event::TripAborted(trip.id));
return;
};
@ -251,6 +253,7 @@ impl TripManager {
);
self.unfinished_trips -= 1;
trip.aborted = true;
self.events.push(Event::TripAborted(trip.id));
return;
};
@ -306,6 +309,7 @@ impl TripManager {
assert!(!trip.finished_at.is_some());
trip.finished_at = Some(now);
self.unfinished_trips -= 1;
self.events.push(Event::TripFinished(trip.id, trip.mode));
}
// If no route is returned, the pedestrian boarded a bus immediately.
@ -385,6 +389,7 @@ impl TripManager {
assert!(!trip.finished_at.is_some());
trip.finished_at = Some(now);
self.unfinished_trips -= 1;
self.events.push(Event::TripFinished(trip.id, trip.mode));
}
pub fn car_or_bike_reached_border(&mut self, now: Duration, car: CarID, i: IntersectionID) {
@ -398,6 +403,7 @@ impl TripManager {
assert!(!trip.finished_at.is_some());
trip.finished_at = Some(now);
self.unfinished_trips -= 1;
self.events.push(Event::TripFinished(trip.id, trip.mode));
}
pub fn abort_trip_failed_start(&mut self, id: TripID) {
@ -405,6 +411,7 @@ impl TripManager {
if !self.trips[id.0].is_bus_trip() {
self.unfinished_trips -= 1;
}
self.events.push(Event::TripAborted(id));
}
pub fn abort_trip_impossible_parking(&mut self, car: CarID) {
@ -412,6 +419,7 @@ impl TripManager {
assert!(!self.trips[trip.0].is_bus_trip());
self.trips[trip.0].aborted = true;
self.unfinished_trips -= 1;
self.events.push(Event::TripAborted(trip));
}
pub fn active_agents(&self) -> Vec<AgentID> {