Use explicit start/endpoints for trips in scenarios. This is necessary

for people that leave one border, then re-enter a different one. #664

Alternative considered: insert a dummy remote trip between the two
borders. We used to do something like this at non-trivial code
complexity expense and having to explain the trip in the UI.

Regenerated all scenarios and prebaked data.

- Modest file size increase, as expected. Montlake scenario goes from
  1.3MB to 1.5, downtown from 37MB to 43MB, all Seattle scenarios from
  280MB to 327MB
- Eyeballed a few of the previously broken trips; they work now!
- Montlake goes from 3127 cancelled trips to just 302!
- No new gridlock, except in Rainier Valley -- disabling that for now
- I discovered missing validation in Poundbury for no-op trips between
  the same building. I'll filter those out and restore prebaked results
  there in a followup.
This commit is contained in:
Dustin Carlino 2021-05-26 14:30:01 -07:00
parent 3b375078b1
commit 9fc79b1089
20 changed files with 747 additions and 777 deletions

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@ pub fn prebake_all() {
MapName::seattle("lakeslice"),
MapName::seattle("phinney"),
MapName::seattle("qa"),
MapName::seattle("rainier_valley"),
//MapName::seattle("rainier_valley"),
MapName::seattle("wallingford"),
] {
let map = map_model::Map::load_synchronously(name.path(), &mut timer);
@ -39,19 +39,21 @@ pub fn prebake_all() {
prebake(&map, scenario, None, &mut timer);
}
for &scenario_name in &["base", "go_active", "base_with_bg", "go_active_with_bg"] {
let map = map_model::Map::load_synchronously(
MapName::new("gb", "poundbury", "center").path(),
&mut timer,
);
let scenario: Scenario = abstio::read_binary(
abstio::path_scenario(map.get_name(), scenario_name),
&mut timer,
);
let mut opts = SimOptions::new("prebaked");
opts.alerts = AlertHandler::Silence;
opts.infinite_parking = true;
prebake(&map, scenario, Some(opts), &mut timer);
if false {
for &scenario_name in &["base", "go_active", "base_with_bg", "go_active_with_bg"] {
let map = map_model::Map::load_synchronously(
MapName::new("gb", "poundbury", "center").path(),
&mut timer,
);
let scenario: Scenario = abstio::read_binary(
abstio::path_scenario(map.get_name(), scenario_name),
&mut timer,
);
let mut opts = SimOptions::new("prebaked");
opts.alerts = AlertHandler::Silence;
opts.infinite_parking = true;
prebake(&map, scenario, Some(opts), &mut timer);
}
}
}

View File

@ -68,7 +68,8 @@ impl GameplayState for Actdev {
.polygon;
for person in app.primary.sim.get_all_people() {
if let TripEndpoint::Bldg(b) = person.home {
if let TripEndpoint::Bldg(b) = app.primary.sim.trip_info(person.trips[0]).start
{
if study_area.contains_pt(app.primary.map.get_b(b).polygon.center()) {
highlight.insert(person.id);
}

View File

@ -372,13 +372,13 @@ pub fn spawn_agents_around(i: IntersectionID, app: &mut App) {
};
scenario.people.push(PersonSpec {
orig_id: None,
origin: TripEndpoint::SuddenlyAppear(Position::new(
lane.id,
Scenario::rand_dist(&mut rng, Distance::ZERO, lane.length()),
)),
trips: vec![IndividTrip::new(
app.primary.sim.time(),
TripPurpose::Shopping,
TripEndpoint::SuddenlyAppear(Position::new(
lane.id,
Scenario::rand_dist(&mut rng, Distance::ZERO, lane.length()),
)),
TripEndpoint::Bldg(map.all_buildings().choose(&mut rng).unwrap().id),
mode,
)],
@ -388,13 +388,13 @@ pub fn spawn_agents_around(i: IntersectionID, app: &mut App) {
for _ in 0..5 {
scenario.people.push(PersonSpec {
orig_id: None,
origin: TripEndpoint::SuddenlyAppear(Position::new(
lane.id,
Scenario::rand_dist(&mut rng, 0.1 * lane.length(), 0.9 * lane.length()),
)),
trips: vec![IndividTrip::new(
app.primary.sim.time(),
TripPurpose::Shopping,
TripEndpoint::SuddenlyAppear(Position::new(
lane.id,
Scenario::rand_dist(&mut rng, 0.1 * lane.length(), 0.9 * lane.length()),
)),
TripEndpoint::Bldg(map.all_buildings().choose(&mut rng).unwrap().id),
TripMode::Walk,
)],

View File

@ -102,10 +102,10 @@ impl State<App> for AgentSpawner {
for _ in 0..self.panel.spinner("number") {
scenario.people.push(PersonSpec {
orig_id: None,
origin: from,
trips: vec![IndividTrip::new(
app.primary.sim.time(),
TripPurpose::Shopping,
from,
to,
self.panel.dropdown_value("mode"),
)],

View File

@ -1077,13 +1077,13 @@ impl TutorialState {
let mut scenario = Scenario::empty(map, "prank");
scenario.people.push(PersonSpec {
orig_id: None,
origin: TripEndpoint::SuddenlyAppear(Position::new(
start_lane,
map.get_l(start_lane).length() * 0.8,
)),
trips: vec![IndividTrip::new(
Time::START_OF_DAY,
TripPurpose::Shopping,
TripEndpoint::SuddenlyAppear(Position::new(
start_lane,
map.get_l(start_lane).length() * 0.8,
)),
TripEndpoint::Bldg(goal_bldg),
TripMode::Drive,
)],
@ -1092,13 +1092,13 @@ impl TutorialState {
for _ in 0..map.get_b(goal_bldg).num_parking_spots() {
scenario.people.push(PersonSpec {
orig_id: None,
origin: TripEndpoint::SuddenlyAppear(Position::new(
lane_near_bldg,
map.get_l(lane_near_bldg).length() / 2.0,
)),
trips: vec![IndividTrip::new(
Time::START_OF_DAY,
TripPurpose::Shopping,
TripEndpoint::SuddenlyAppear(Position::new(
lane_near_bldg,
map.get_l(lane_near_bldg).length() / 2.0,
)),
TripEndpoint::Bldg(goal_bldg),
TripMode::Drive,
)],

View File

@ -60,7 +60,8 @@ fn add_return_trips(scenario: &mut Scenario, rng: &mut XorShiftRng) {
person.trips.push(IndividTrip::new(
depart,
TripPurpose::Home,
person.origin,
person.trips[0].destination,
person.trips[0].origin,
person.trips[0].mode,
));
cnt += 1;
@ -92,7 +93,7 @@ fn add_lunch_trips(scenario: &mut Scenario, map: &Map, rng: &mut XorShiftRng, ti
timer.next();
let num_trips = person.trips.len();
// Only handle people with their final trip going back home.
if num_trips <= 1 || person.trips[num_trips - 1].destination != person.origin {
if num_trips <= 1 || person.trips[num_trips - 1].destination != person.trips[0].origin {
continue;
}
@ -116,12 +117,14 @@ fn add_lunch_trips(scenario: &mut Scenario, map: &Map, rng: &mut XorShiftRng, ti
person.trips.push(IndividTrip::new(
depart,
TripPurpose::Meal,
TripEndpoint::Bldg(work),
TripEndpoint::Bldg(restaurant),
mode,
));
person.trips.push(IndividTrip::new(
depart + Duration::minutes(30),
TripPurpose::Work,
TripEndpoint::Bldg(restaurant),
TripEndpoint::Bldg(work),
mode,
));

View File

@ -54,9 +54,9 @@ fn parse_trips(csv_path: String) -> Result<Vec<ExternalPerson>> {
// For each row in the CSV file, create a person who takes a single trip from the origin to
// the destination. They do not take a later trip to return home.
people.push(ExternalPerson {
origin: ExternalTripEndpoint::Position(origin),
trips: vec![ExternalTrip {
departure,
origin: ExternalTripEndpoint::Position(origin),
destination: ExternalTripEndpoint::Position(destination),
mode,
purpose: TripPurpose::Work,

View File

@ -179,21 +179,18 @@ pub fn make_weekday_scenario(
huge_map: &Map,
timer: &mut Timer,
) -> Scenario {
// Include the origin with each trip
let mut individ_trips: Vec<Option<(TripEndpoint, IndividTrip)>> = Vec::new();
let mut individ_trips: Vec<Option<IndividTrip>> = Vec::new();
// person -> (trip seq, index into individ_trips)
let mut trips_per_person: MultiMap<OrigPersonID, ((usize, bool, usize), usize)> =
MultiMap::new();
for trip in clip_trips(map, popdat, huge_map, timer) {
let idx = individ_trips.len();
individ_trips.push(Some((
individ_trips.push(Some(IndividTrip::new(
trip.orig.depart_at,
trip.orig.purpose,
trip.from,
IndividTrip::new(
trip.orig.depart_at,
trip.orig.purpose,
trip.to,
trip.orig.mode,
),
trip.to,
trip.orig.mode,
)));
trips_per_person.insert(trip.orig.person, (trip.orig.seq, idx));
}
@ -205,17 +202,17 @@ pub fn make_weekday_scenario(
let mut people = Vec::new();
for (orig_id, seq_trips) in trips_per_person.consume() {
let mut pairs = Vec::new();
let mut trips = Vec::new();
for (_, idx) in seq_trips {
pairs.push(individ_trips[idx].take().unwrap());
trips.push(individ_trips[idx].take().unwrap());
}
// Actually, the sequence in the Soundcast dataset crosses midnight. Don't do that; sort by
// departure time starting with midnight.
pairs.sort_by_key(|(_, t)| t.depart);
trips.sort_by_key(|t| t.depart);
// Sanity check that endpoints match up
for pair in pairs.windows(2) {
let destination = &pair[0].1.destination;
let origin = &pair[1].0;
for pair in trips.windows(2) {
let destination = &pair[0].destination;
let origin = &pair[1].origin;
if destination != origin {
warn!(
"Skipping {:?}, with adjacent trips that warp from {:?} to {:?}",
@ -227,8 +224,7 @@ pub fn make_weekday_scenario(
people.push(PersonSpec {
orig_id: Some(orig_id),
origin: pairs[0].0,
trips: pairs.into_iter().map(|(_, t)| t).collect(),
trips,
});
}
for maybe_t in individ_trips {

View File

@ -98,7 +98,7 @@ pub async fn generate_scenario(
// Remove people from the scenario we just generated that live in the study area. The
// data imported using importer/actdev_scenarios.sh already covers them.
let before = scenario.people.len();
scenario.people.retain(|p| match p.origin {
scenario.people.retain(|p| match p.trips[0].origin {
TripEndpoint::Bldg(b) => !study_area.contains_pt(map.get_b(b).polygon.center()),
_ => true,
});

View File

@ -184,7 +184,6 @@ impl PersonFactory {
let mut output = PersonSpec {
orig_id: None,
origin: TripEndpoint::Bldg(person.home),
trips: Vec::new(),
};
@ -204,9 +203,13 @@ impl PersonFactory {
};
let mode = pick_mode(current_location, goto, map, rng, config);
output
.trips
.push(IndividTrip::new(departure_time, purpose, goto, mode));
output.trips.push(IndividTrip::new(
departure_time,
purpose,
current_location,
goto,
mode,
));
current_location = goto;
}

View File

@ -87,7 +87,7 @@ pub fn disaggregate(
for _ in 0..desire.number_commuters {
// Pick a specific home and workplace. It might be off-map, depending on how much the
// zone overlaps the map.
if let (Some((leave_home, goto_home)), Some((_, goto_work))) = (
if let (Some((leave_home, goto_home)), Some((leave_work, goto_work))) = (
home_zone.pick_home(desire.mode, map, rng),
work_zone.pick_workplace(desire.mode, map, rng),
) {
@ -96,12 +96,18 @@ pub fn disaggregate(
let return_home_time = goto_work_time + opts.work_duration.sample(rng);
people.push(PersonSpec {
orig_id: None,
origin: leave_home,
trips: vec![
IndividTrip::new(goto_work_time, TripPurpose::Work, goto_work, desire.mode),
IndividTrip::new(
goto_work_time,
TripPurpose::Work,
leave_home,
goto_work,
desire.mode,
),
IndividTrip::new(
return_home_time,
TripPurpose::Home,
leave_work,
goto_home,
desire.mode,
),

View File

@ -272,10 +272,9 @@ fn create_prole(
Ok(PersonSpec {
orig_id: None,
origin: home,
trips: vec![
IndividTrip::new(depart_am, TripPurpose::Work, work, mode),
IndividTrip::new(depart_pm, TripPurpose::Home, home, mode),
IndividTrip::new(depart_am, TripPurpose::Work, home, work, mode),
IndividTrip::new(depart_pm, TripPurpose::Home, work, home, mode),
],
})
}

View File

@ -11,13 +11,13 @@ use crate::{IndividTrip, PersonSpec, TripEndpoint, TripMode, TripPurpose};
#[derive(Deserialize)]
pub struct ExternalPerson {
pub origin: ExternalTripEndpoint,
pub trips: Vec<ExternalTrip>,
}
#[derive(Deserialize)]
pub struct ExternalTrip {
pub departure: Time,
pub origin: ExternalTripEndpoint,
pub destination: ExternalTripEndpoint,
pub mode: TripMode,
pub purpose: TripPurpose,
@ -74,25 +74,23 @@ impl ExternalPerson {
for person in input {
let mut spec = PersonSpec {
orig_id: None,
origin: match lookup_pt(person.origin, true, person.trips[0].mode) {
Ok(endpt) => endpt,
Err(err) => {
if skip_problems {
warn!("Skipping person: {}", err);
continue;
} else {
return Err(err);
}
}
},
trips: Vec::new(),
};
for trip in person.trips {
spec.trips.push(IndividTrip::new(
trip.departure,
trip.purpose,
// TODO Do we handle somebody going off-map via one one-way bridge, and
// re-entering using the other?
match lookup_pt(trip.origin, true, trip.mode) {
Ok(endpt) => endpt,
Err(err) => {
if skip_problems {
warn!("Skipping person: {}", err);
continue;
} else {
return Err(err);
}
}
},
match lookup_pt(trip.destination, false, trip.mode) {
Ok(endpt) => endpt,
Err(err) => {

View File

@ -163,10 +163,10 @@ impl SpawnOverTime {
};
scenario.people.push(PersonSpec {
orig_id: None,
origin: TripEndpoint::Bldg(from_bldg),
trips: vec![IndividTrip::new(
depart,
TripPurpose::Shopping,
TripEndpoint::Bldg(from_bldg),
self.goal.unwrap_or_else(|| {
TripEndpoint::Bldg(map.all_buildings().choose(rng).unwrap().id)
}),
@ -181,10 +181,10 @@ impl BorderSpawnOverTime {
let depart = rand_time(rng, self.start_time, self.stop_time);
scenario.people.push(PersonSpec {
orig_id: None,
origin: TripEndpoint::Border(self.start_from_border),
trips: vec![IndividTrip::new(
depart,
TripPurpose::Shopping,
TripEndpoint::Border(self.start_from_border),
self.goal.unwrap_or_else(|| {
TripEndpoint::Bldg(map.all_buildings().choose(rng).unwrap().id)
}),

View File

@ -33,15 +33,16 @@ pub struct Scenario {
pub struct PersonSpec {
/// Just used for debugging
pub orig_id: Option<OrigPersonID>,
/// The first trip starts here
pub origin: TripEndpoint,
/// Each trip starts at the destination of the previous trip
/// There must be continuity between trips: each trip starts at the destination of the previous
/// trip. In the case of borders, the outbound and inbound border may be different. This means
/// that there was some sort of "remote" trip happening outside the map that we don't simulate.
pub trips: Vec<IndividTrip>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct IndividTrip {
pub depart: Time,
pub origin: TripEndpoint,
pub destination: TripEndpoint,
pub mode: TripMode,
pub purpose: TripPurpose,
@ -54,11 +55,13 @@ impl IndividTrip {
pub fn new(
depart: Time,
purpose: TripPurpose,
origin: TripEndpoint,
destination: TripEndpoint,
mode: TripMode,
) -> IndividTrip {
IndividTrip {
depart,
origin,
destination,
mode,
purpose,
@ -152,23 +155,17 @@ impl Scenario {
let (vehicle_specs, cars_initially_parked_at, vehicle_foreach_trip) =
p.get_vehicles(rng);
let person = sim.new_person(
p.orig_id,
p.origin,
Scenario::rand_ped_speed(rng),
vehicle_specs,
);
let person = sim.new_person(p.orig_id, Scenario::rand_ped_speed(rng), vehicle_specs);
for (idx, b) in cars_initially_parked_at {
parked_cars.push((person.vehicles[idx].clone(), b));
}
let mut from = p.origin;
for (trip, maybe_idx) in p.trips.iter().zip(vehicle_foreach_trip) {
schedule_trips.push((
person.id,
TripInfo {
departure: trip.depart,
mode: trip.mode,
start: from,
start: trip.origin,
end: trip.destination,
purpose: trip.purpose,
modified: trip.modified,
@ -184,7 +181,6 @@ impl Scenario {
use_vehicle: maybe_idx.map(|idx| person.vehicles[idx].id),
},
));
from = trip.destination;
}
}
@ -416,18 +412,19 @@ impl PersonSpec {
pair[1].depart
);
}
}
let mut endpts = vec![self.origin];
for t in &self.trips {
endpts.push(t.destination);
}
for pair in endpts.windows(2) {
if pair[0] == pair[1] {
if pair[0].destination != pair[1].origin {
// Exiting one border and re-entering another is fine
if matches!(pair[0].destination, TripEndpoint::Border(_))
&& matches!(pair[1].origin, TripEndpoint::Border(_))
{
continue;
}
bail!(
"Person ({:?}) has two adjacent trips between the same place: {:?}",
"Person ({:?}) warps from {:?} to {:?} during adjacent trips",
self.orig_id,
pair[0]
pair[0].destination,
pair[1].origin
);
}
}
@ -452,7 +449,6 @@ impl PersonSpec {
let mut car_locations: Vec<(usize, Option<BuildingID>)> = Vec::new();
// TODO If the trip is cancelled, this should be affected...
let mut from = self.origin;
for trip in &self.trips {
let use_for_trip = match trip.mode {
TripMode::Walk | TripMode::Transit => None,
@ -464,7 +460,7 @@ impl PersonSpec {
bike_idx
}
TripMode::Drive => {
let need_parked_at = match from {
let need_parked_at = match trip.origin {
TripEndpoint::Bldg(b) => Some(b),
_ => None,
};
@ -500,7 +496,6 @@ impl PersonSpec {
Some(idx)
}
};
from = trip.destination;
vehicle_foreach_trip.push(use_for_trip);
}

View File

@ -16,7 +16,7 @@ pub(crate) struct TrafficRecorder {
capture_points: BTreeSet<IntersectionID>,
// TODO The RNG will determine vehicle length, so this won't be a perfect capture. Hopefully
// good enough.
trips: Vec<(TripEndpoint, IndividTrip)>,
trips: Vec<IndividTrip>,
seen_trips: BTreeSet<TripID>,
}
@ -40,18 +40,16 @@ impl TrafficRecorder {
for step in driving.get_path(*car).unwrap().get_steps() {
if let PathStep::Turn(t) = step {
if self.capture_points.contains(&t.parent) {
self.trips.push((
self.trips.push(IndividTrip::new(
time,
TripPurpose::Shopping,
TripEndpoint::SuddenlyAppear(Position::start(*l)),
IndividTrip::new(
time,
TripPurpose::Shopping,
TripEndpoint::Border(t.parent),
if car.vehicle_type == VehicleType::Bike {
TripMode::Bike
} else {
TripMode::Drive
},
),
TripEndpoint::Border(t.parent),
if car.vehicle_type == VehicleType::Bike {
TripMode::Bike
} else {
TripMode::Drive
},
));
self.seen_trips.insert(*trip);
return;
@ -69,10 +67,9 @@ impl TrafficRecorder {
pub fn save(mut self, map: &Map) {
let mut people = Vec::new();
for (origin, trip) in self.trips.drain(..) {
for trip in self.trips.drain(..) {
people.push(PersonSpec {
orig_id: None,
origin,
trips: vec![trip],
});
}

View File

@ -22,9 +22,9 @@ use crate::{
AgentID, AlertLocation, Analytics, CapSimState, CarID, Command, CreateCar, DrivingSimState,
Event, IntersectionSimState, OrigPersonID, PandemicModel, ParkedCar, ParkingSim,
ParkingSimState, ParkingSpot, Person, PersonID, Router, Scheduler, SidewalkPOI, SidewalkSpot,
StartTripArgs, TrafficRecorder, TransitSimState, TripEndpoint, TripID, TripInfo, TripManager,
TripPhaseType, Vehicle, VehicleSpec, VehicleType, WalkingSimState, BUS_LENGTH,
LIGHT_RAIL_LENGTH, MIN_CAR_LENGTH,
StartTripArgs, TrafficRecorder, TransitSimState, TripID, TripInfo, TripManager, TripPhaseType,
Vehicle, VehicleSpec, VehicleType, WalkingSimState, BUS_LENGTH, LIGHT_RAIL_LENGTH,
MIN_CAR_LENGTH,
};
mod queries;
@ -317,12 +317,10 @@ impl Sim {
pub(crate) fn new_person(
&mut self,
orig_id: Option<OrigPersonID>,
home: TripEndpoint,
ped_speed: Speed,
vehicle_specs: Vec<VehicleSpec>,
) -> &Person {
self.trips
.new_person(orig_id, home, ped_speed, vehicle_specs)
self.trips.new_person(orig_id, ped_speed, vehicle_specs)
}
pub(crate) fn seed_parked_car(&mut self, vehicle: Vehicle, spot: ParkingSpot) {
self.parking.reserve_spot(spot, vehicle.id);

View File

@ -60,7 +60,6 @@ impl TripManager {
pub fn new_person(
&mut self,
orig_id: Option<OrigPersonID>,
home: TripEndpoint,
ped_speed: Speed,
vehicle_specs: Vec<VehicleSpec>,
) -> &Person {
@ -78,7 +77,6 @@ impl TripManager {
self.people.push(Person {
id,
orig_id,
home,
trips: Vec::new(),
// The first new_trip will set this properly.
state: PersonState::OffMap,
@ -1341,7 +1339,6 @@ impl TripManager {
for p in &self.people {
scenario.people.push(PersonSpec {
orig_id: p.orig_id,
origin: self.trips[p.trips[0].0].info.start,
trips: p
.trips
.iter()
@ -1350,6 +1347,7 @@ impl TripManager {
IndividTrip::new(
trip.info.departure,
trip.info.purpose,
trip.info.start,
trip.info.end,
trip.info.mode,
)
@ -1511,7 +1509,6 @@ impl<T> TripResult<T> {
pub struct Person {
pub id: PersonID,
pub orig_id: Option<OrigPersonID>,
pub home: TripEndpoint,
pub trips: Vec<TripID>,
pub state: PersonState,

View File

@ -192,12 +192,12 @@ fn test_lane_changing(map: &Map) -> Result<()> {
for (idx, (from, to)) in od.into_iter().enumerate() {
scenario.people.push(PersonSpec {
orig_id: None,
origin: TripEndpoint::Border(from),
trips: vec![IndividTrip::new(
// Space out the spawn times a bit. If a vehicle tries to spawn and something's in
// the way, there's a fixed retry time in the simulation that we'll hit.
Time::START_OF_DAY + Duration::seconds(idx as f64 - 0.5).max(Duration::ZERO),
TripPurpose::Shopping,
TripEndpoint::Border(from),
TripEndpoint::Border(to),
// About half cars, half bikes
if idx % 2 == 0 {