checkbox to toggle running the pandemic model in the population map

This commit is contained in:
Dustin Carlino 2020-03-31 10:33:59 -07:00
parent b16aceaa9a
commit c49fc20a88
11 changed files with 129 additions and 38 deletions

View File

@ -28,7 +28,7 @@ pub enum Overlays {
Elevation(Colorer, Drawable), Elevation(Colorer, Drawable),
Edits(Colorer), Edits(Colorer),
TripsHistogram(Time, Composite), TripsHistogram(Time, Composite),
PopulationMap(Time, Option<HeatmapOptions>, Drawable, Composite), PopulationMap(Time, PopulationOptions, Drawable, Composite),
// These aren't selectable from the main picker; they're particular to some object. // These aren't selectable from the main picker; they're particular to some object.
// TODO They should become something else, like an info panel tab. // TODO They should become something else, like an info panel tab.
@ -160,7 +160,7 @@ impl Overlays {
_ => unreachable!(), _ => unreachable!(),
}, },
None => { None => {
let new_opts = heatmap_options(c); let new_opts = population_options(c);
if *opts != new_opts { if *opts != new_opts {
app.overlay = Overlays::population_map(ctx, app, new_opts); app.overlay = Overlays::population_map(ctx, app, new_opts);
} }
@ -243,7 +243,6 @@ impl Overlays {
Btn::text_fg("bus network").build_def(ctx, hotkey(Key::U)), Btn::text_fg("bus network").build_def(ctx, hotkey(Key::U)),
Btn::text_fg("population map").build_def(ctx, hotkey(Key::X)), Btn::text_fg("population map").build_def(ctx, hotkey(Key::X)),
]; ];
// TODO Grey out the inactive SVGs, and add the green checkmark
if let Some(name) = match app.overlay { if let Some(name) = match app.overlay {
Overlays::Inactive => Some("None"), Overlays::Inactive => Some("None"),
Overlays::ParkingAvailability(_, _) => Some("parking availability"), Overlays::ParkingAvailability(_, _) => Some("parking availability"),
@ -351,7 +350,14 @@ impl Overlays {
.maybe_cb( .maybe_cb(
"population map", "population map",
Box::new(|ctx, app| { Box::new(|ctx, app| {
app.overlay = Overlays::population_map(ctx, app, Some(HeatmapOptions::new())); app.overlay = Overlays::population_map(
ctx,
app,
PopulationOptions {
pandemic: false,
heatmap: Some(HeatmapOptions::new()),
},
);
Some(maybe_unzoom(ctx, app)) Some(maybe_unzoom(ctx, app))
}), }),
); );
@ -813,7 +819,11 @@ impl Overlays {
// TODO Disable drawing unzoomed agents... or alternatively, implement this by asking Sim to // TODO Disable drawing unzoomed agents... or alternatively, implement this by asking Sim to
// return this kind of data instead! // return this kind of data instead!
fn population_map(ctx: &mut EventCtx, app: &App, opts: Option<HeatmapOptions>) -> Overlays { fn population_map(ctx: &mut EventCtx, app: &App, opts: PopulationOptions) -> Overlays {
if opts.pandemic {
return Overlays::pandemic_map(ctx, app, opts);
}
let mut pts = Vec::new(); let mut pts = Vec::new();
// Faster to grab all agent positions than individually map trips to agent positions. // Faster to grab all agent positions than individually map trips to agent positions.
for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) { for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) {
@ -839,14 +849,68 @@ impl Overlays {
// It's quite silly to produce triangles for the same circle over and over again. ;) // It's quite silly to produce triangles for the same circle over and over again. ;)
let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon(); let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon();
let mut batch = GeomBatch::new(); let mut batch = GeomBatch::new();
if let Some(ref o) = opts { if let Some(ref o) = opts.heatmap {
make_heatmap(&mut batch, app.primary.map.get_bounds(), pts, o); make_heatmap(&mut batch, app.primary.map.get_bounds(), pts, o);
} else { } else {
for pt in pts { for pt in pts {
batch.push(Color::RED.alpha(0.8), circle.translate(pt.x(), pt.y())); batch.push(Color::RED.alpha(0.8), circle.translate(pt.x(), pt.y()));
} }
} }
let controls = population_controls(ctx, app, opts.as_ref()); let controls = population_controls(ctx, app, &opts, None);
Overlays::PopulationMap(app.primary.sim.time(), opts, ctx.upload(batch), controls)
}
// Similar to population_map, but only display infected people.
fn pandemic_map(ctx: &mut EventCtx, app: &App, opts: PopulationOptions) -> Overlays {
// TODO Why not app.primary.current_flags.sim_flags.make_rng()? Because that'll only be the
// same every time this code runs (frequently, as the simulation is run) if --rng_seed is
// specified in the flags. If you forget it, quite confusing to see the model jump around.
use rand::SeedableRng;
use rand_xorshift::XorShiftRng;
let model = PandemicModel::calculate(
app.primary.sim.get_analytics(),
app.primary.sim.time(),
&mut XorShiftRng::from_seed([42; 16]),
);
let mut infected_pts = Vec::new();
// Faster to grab all agent positions than individually map trips to agent positions.
for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) {
if let Some(p) = a.person {
if model.infected.contains(&p) {
infected_pts.push(a.pos);
}
}
}
let mut seen_bldgs = HashSet::new();
for person in app.primary.sim.get_all_people() {
match person.state {
// Already covered above
PersonState::Trip(_) => {}
PersonState::Inside(b) => {
// Duplicate circles for the same building are expensive!
if model.infected.contains(&person.id) && !seen_bldgs.contains(&b) {
seen_bldgs.insert(b);
infected_pts.push(app.primary.map.get_b(b).polygon.center());
}
}
PersonState::OffMap | PersonState::Limbo => {}
}
}
// It's quite silly to produce triangles for the same circle over and over again. ;)
let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon();
let mut batch = GeomBatch::new();
if let Some(ref o) = opts.heatmap {
make_heatmap(&mut batch, app.primary.map.get_bounds(), infected_pts, o);
} else {
for pt in infected_pts {
batch.push(Color::RED.alpha(0.8), circle.translate(pt.x(), pt.y()));
}
}
let controls = population_controls(ctx, app, &opts, Some(model));
Overlays::PopulationMap(app.primary.sim.time(), opts, ctx.upload(batch), controls) Overlays::PopulationMap(app.primary.sim.time(), opts, ctx.upload(batch), controls)
} }
} }
@ -864,8 +928,20 @@ fn maybe_unzoom(ctx: &EventCtx, app: &mut App) -> Transition {
)) ))
} }
#[derive(Clone, PartialEq)]
pub struct PopulationOptions {
pandemic: bool,
// If None, just a dot map
heatmap: Option<HeatmapOptions>,
}
// This function sounds more ominous than it should. // This function sounds more ominous than it should.
fn population_controls(ctx: &mut EventCtx, app: &App, opts: Option<&HeatmapOptions>) -> Composite { fn population_controls(
ctx: &mut EventCtx,
app: &App,
opts: &PopulationOptions,
pandemic: Option<PandemicModel>,
) -> Composite {
let (total_ppl, ppl_in_bldg, ppl_off_map) = app.primary.sim.num_ppl(); let (total_ppl, ppl_in_bldg, ppl_off_map) = app.primary.sim.num_ppl();
let mut col = vec![ let mut col = vec![
@ -884,22 +960,10 @@ fn population_controls(ctx: &mut EventCtx, app: &App, opts: Option<&HeatmapOptio
format!("Off-map: {}", prettyprint_usize(ppl_off_map)).draw_text(ctx), format!("Off-map: {}", prettyprint_usize(ppl_off_map)).draw_text(ctx),
]) ])
.centered(), .centered(),
Widget::checkbox(ctx, "Show heatmap", None, opts.is_some()), Widget::checkbox(ctx, "Run pandemic model", None, opts.pandemic),
]; ];
// TODO tmp place to put pandemic model if let Some(model) = pandemic {
if app.opts.dev {
// TODO Why not app.primary.current_flags.sim_flags.make_rng()? Because that'll only be the
// same every time this code runs (frequently, as the simulation is run) if --rng_seed is
// specified in the flags. If you forget it, quite confusing to see the model jump around.
use rand::SeedableRng;
use rand_xorshift::XorShiftRng;
let model = PandemicModel::calculate(
app.primary.sim.get_analytics(),
app.primary.sim.time(),
&mut XorShiftRng::from_seed([42; 16]),
);
col.push( col.push(
format!( format!(
"Pandemic model: {} infected ({:.1}%)", "Pandemic model: {} infected ({:.1}%)",
@ -910,7 +974,13 @@ fn population_controls(ctx: &mut EventCtx, app: &App, opts: Option<&HeatmapOptio
); );
} }
if let Some(ref o) = opts { col.push(Widget::checkbox(
ctx,
"Show heatmap",
None,
opts.heatmap.is_some(),
));
if let Some(ref o) = opts.heatmap {
// TODO Display the value... // TODO Display the value...
col.push(Widget::row(vec![ col.push(Widget::row(vec![
"Resolution (meters)".draw_text(ctx).margin(5), "Resolution (meters)".draw_text(ctx).margin(5),
@ -942,8 +1012,8 @@ fn population_controls(ctx: &mut EventCtx, app: &App, opts: Option<&HeatmapOptio
.build(ctx) .build(ctx)
} }
fn heatmap_options(c: &mut Composite) -> Option<HeatmapOptions> { fn population_options(c: &mut Composite) -> PopulationOptions {
if c.is_checked("Show heatmap") { let heatmap = if c.is_checked("Show heatmap") {
// Did we just change? // Did we just change?
if c.has_widget("resolution") { if c.has_widget("resolution") {
Some(HeatmapOptions { Some(HeatmapOptions {
@ -956,5 +1026,9 @@ fn heatmap_options(c: &mut Composite) -> Option<HeatmapOptions> {
} }
} else { } else {
None None
};
PopulationOptions {
pandemic: c.is_checked("Run pandemic model"),
heatmap,
} }
} }

View File

@ -30,9 +30,9 @@ impl TitleScreen {
Composite::new( Composite::new(
Widget::col(vec![ Widget::col(vec![
Widget::draw_svg(ctx, "../data/system/assets/pregame/logo.svg").margin(5), Widget::draw_svg(ctx, "../data/system/assets/pregame/logo.svg").margin(5),
// TODO that nicer font
// TODO Any key // TODO Any key
// TODO The hover color is wacky Btn::text_bg2("PLAY")
Btn::svg_def("../data/system/assets/pregame/start.svg")
.build(ctx, "start game", hotkeys(vec![Key::Space, Key::Enter])) .build(ctx, "start game", hotkeys(vec![Key::Space, Key::Enter]))
.margin(5), .margin(5),
]) ])

View File

@ -487,6 +487,7 @@ pub struct CreateCar {
pub maybe_parked_car: Option<ParkedCar>, pub maybe_parked_car: Option<ParkedCar>,
// None for buses // None for buses
pub trip: Option<TripID>, pub trip: Option<TripID>,
pub person: Option<PersonID>,
} }
impl CreateCar { impl CreateCar {
@ -496,6 +497,7 @@ impl CreateCar {
router: Router, router: Router,
req: PathRequest, req: PathRequest,
trip: TripID, trip: TripID,
person: PersonID,
) -> CreateCar { ) -> CreateCar {
CreateCar { CreateCar {
vehicle, vehicle,
@ -504,6 +506,7 @@ impl CreateCar {
start_dist: start_pos.dist_along(), start_dist: start_pos.dist_along(),
maybe_parked_car: None, maybe_parked_car: None,
trip: Some(trip), trip: Some(trip),
person: Some(person),
} }
} }
@ -514,6 +517,7 @@ impl CreateCar {
req: PathRequest, req: PathRequest,
start_dist: Distance, start_dist: Distance,
trip: TripID, trip: TripID,
person: PersonID,
) -> CreateCar { ) -> CreateCar {
CreateCar { CreateCar {
vehicle: parked_car.vehicle.clone(), vehicle: parked_car.vehicle.clone(),
@ -522,6 +526,7 @@ impl CreateCar {
start_dist, start_dist,
maybe_parked_car: Some(parked_car), maybe_parked_car: Some(parked_car),
trip: Some(trip), trip: Some(trip),
person: Some(person),
} }
} }
} }

View File

@ -292,7 +292,9 @@ impl TripSpawner {
scheduler.quick_push( scheduler.quick_push(
start_time, start_time,
Command::SpawnCar( Command::SpawnCar(
CreateCar::for_appearing(vehicle, start_pos, router, req, trip), CreateCar::for_appearing(
vehicle, start_pos, router, req, trip, person,
),
retry_if_no_room, retry_if_no_room,
), ),
); );

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
CarStatus, DistanceInterval, DrawCarInput, ParkingSpot, Router, TimeInterval, TransitSimState, CarStatus, DistanceInterval, DrawCarInput, ParkingSpot, PersonID, Router, TimeInterval,
TripID, Vehicle, VehicleType, TransitSimState, TripID, Vehicle, VehicleType,
}; };
use geom::{Distance, Duration, PolyLine, Time}; use geom::{Distance, Duration, PolyLine, Time};
use map_model::{Map, Traversable}; use map_model::{Map, Traversable};
@ -14,6 +14,7 @@ pub struct Car {
pub router: Router, pub router: Router,
// None for buses // None for buses
pub trip: Option<TripID>, pub trip: Option<TripID>,
pub person: Option<PersonID>,
pub started_at: Time, pub started_at: Time,
pub total_blocked_time: Duration, pub total_blocked_time: Duration,

View File

@ -98,6 +98,7 @@ impl DrivingSimState {
started_at: now, started_at: now,
total_blocked_time: Duration::ZERO, total_blocked_time: Duration::ZERO,
trip: params.trip, trip: params.trip,
person: params.person,
}; };
if let Some(p) = params.maybe_parked_car { if let Some(p) = params.maybe_parked_car {
car.state = CarState::Unparking( car.state = CarState::Unparking(
@ -782,6 +783,7 @@ impl DrivingSimState {
result.push(UnzoomedAgent { result.push(UnzoomedAgent {
vehicle_type: Some(car.vehicle.vehicle_type), vehicle_type: Some(car.vehicle.vehicle_type),
pos: queue.id.dist_along(dist, map).0, pos: queue.id.dist_along(dist, map).0,
person: car.person,
}); });
} }
} }

View File

@ -1,8 +1,8 @@
use crate::{ use crate::{
AgentID, Command, CreatePedestrian, DistanceInterval, DrawPedCrowdInput, DrawPedestrianInput, AgentID, Command, CreatePedestrian, DistanceInterval, DrawPedCrowdInput, DrawPedestrianInput,
Event, IntersectionSimState, ParkingSimState, ParkingSpot, PedCrowdLocation, PedestrianID, Event, IntersectionSimState, ParkingSimState, ParkingSpot, PedCrowdLocation, PedestrianID,
Scheduler, SidewalkPOI, SidewalkSpot, TimeInterval, TransitSimState, TripID, TripManager, PersonID, Scheduler, SidewalkPOI, SidewalkSpot, TimeInterval, TransitSimState, TripID,
TripPositions, UnzoomedAgent, TripManager, TripPositions, UnzoomedAgent,
}; };
use abstutil::{deserialize_multimap, serialize_multimap, MultiMap}; use abstutil::{deserialize_multimap, serialize_multimap, MultiMap};
use geom::{Distance, Duration, Line, PolyLine, Speed, Time}; use geom::{Distance, Duration, Line, PolyLine, Speed, Time};
@ -64,6 +64,7 @@ impl WalkingSimState {
path: params.path, path: params.path,
goal: params.goal, goal: params.goal,
trip: params.trip, trip: params.trip,
person: params.person,
}; };
ped.state = match params.start.connection { ped.state = match params.start.connection {
SidewalkPOI::Building(b) | SidewalkPOI::ParkingSpot(ParkingSpot::Offstreet(b, _)) => { SidewalkPOI::Building(b) | SidewalkPOI::ParkingSpot(ParkingSpot::Offstreet(b, _)) => {
@ -330,6 +331,7 @@ impl WalkingSimState {
peds.push(UnzoomedAgent { peds.push(UnzoomedAgent {
vehicle_type: None, vehicle_type: None,
pos: ped.get_draw_ped(now, map).pos, pos: ped.get_draw_ped(now, map).pos,
person: Some(ped.person),
}); });
} }
@ -459,6 +461,7 @@ struct Pedestrian {
path: Path, path: Path,
goal: SidewalkSpot, goal: SidewalkSpot,
trip: TripID, trip: TripID,
person: PersonID,
} }
impl Pedestrian { impl Pedestrian {

View File

@ -22,14 +22,14 @@ impl PandemicModel {
// //
// This recomputes everything every time the UI asks for it. That's fine for the scale of // This recomputes everything every time the UI asks for it. That's fine for the scale of
// simulations now; everything else in Analytics works the same way. The faster streaming // simulations now; everything else in Analytics works the same way. The faster streaming
// version is very straightforward -- cache this output and only process new events to update. // version is very straightforward -- cache this output and only process new events.
pub fn calculate(analytics: &Analytics, now: Time, rng: &mut XorShiftRng) -> PandemicModel { pub fn calculate(analytics: &Analytics, now: Time, rng: &mut XorShiftRng) -> PandemicModel {
let mut state = PandemicModel { let mut state = PandemicModel {
infected: BTreeSet::new(), infected: BTreeSet::new(),
bldg_occupants: BTreeMap::new(), bldg_occupants: BTreeMap::new(),
}; };
// Now track people's movements through buildings // Track people's movements through buildings
for (time, person, bldg, left) in &analytics.building_transitions { for (time, person, bldg, left) in &analytics.building_transitions {
if *time > now { if *time > now {
break; break;
@ -49,7 +49,8 @@ impl PandemicModel {
true true
} }
}); });
// TODO Same bug as above. // TODO A person left a building, but they weren't inside of it? Bug -- few
// possible causes...
if inside_since.is_none() { if inside_since.is_none() {
continue; continue;
} }
@ -57,7 +58,6 @@ impl PandemicModel {
// Was this person leaving infected while they were inside? // Was this person leaving infected while they were inside?
if !state.infected.contains(person) { if !state.infected.contains(person) {
//let time_in_bldg = time - inside_since.unwrap();
let mut longest_overlap_with_infected = Duration::ZERO; let mut longest_overlap_with_infected = Duration::ZERO;
for (p, t) in &state.bldg_occupants[bldg] { for (p, t) in &state.bldg_occupants[bldg] {
if !state.infected.contains(p) { if !state.infected.contains(p) {

View File

@ -1,4 +1,4 @@
use crate::{CarID, PedestrianID, VehicleType}; use crate::{CarID, PedestrianID, PersonID, VehicleType};
use geom::{Angle, Distance, PolyLine, Pt2D, Time}; use geom::{Angle, Distance, PolyLine, Pt2D, Time};
use map_model::{BuildingID, Map, Traversable, TurnID}; use map_model::{BuildingID, Map, Traversable, TurnID};
@ -50,6 +50,8 @@ pub struct UnzoomedAgent {
// None means a pedestrian. // None means a pedestrian.
pub vehicle_type: Option<VehicleType>, pub vehicle_type: Option<VehicleType>,
pub pos: Pt2D, pub pos: Pt2D,
// None means a bus.
pub person: Option<PersonID>,
} }
// TODO Can we return borrows instead? Nice for time travel, not for main sim? // TODO Can we return borrows instead? Nice for time travel, not for main sim?

View File

@ -257,6 +257,7 @@ impl Sim {
router: Router::follow_bus_route(path.clone(), end_dist), router: Router::follow_bus_route(path.clone(), end_dist),
maybe_parked_car: None, maybe_parked_car: None,
trip: None, trip: None,
person: None,
}, },
map, map,
&self.intersections, &self.intersections,

View File

@ -259,6 +259,7 @@ impl TripManager {
req, req,
start.dist_along(), start.dist_along(),
trip.id, trip.id,
trip.person,
), ),
true, true,
), ),
@ -313,7 +314,7 @@ impl TripManager {
scheduler.push( scheduler.push(
now, now,
Command::SpawnCar( Command::SpawnCar(
CreateCar::for_appearing(vehicle, driving_pos, router, req, trip.id), CreateCar::for_appearing(vehicle, driving_pos, router, req, trip.id, trip.person),
true, true,
), ),
); );