mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-27 15:03:20 +03:00
Split the "input" layer of the sim crate into a synthetic population
crate, in preparation for future focus on travel demand models that incorporate more per-person info. Note: I'm also a bit tempted to try to further split sim into a "high-level" layer that orchestrates spawning and different legs of a trip, from the "low-level" layer that moves pedestrians and vehicles. That could _possibly_ pave the way for someday using a different traffic simulation backend with more realistic movement mechanics. Step 1: just get the synthpop crate to build
This commit is contained in:
parent
6c2a581be5
commit
e45a5181db
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -3779,6 +3779,19 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synthpop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"abstio",
|
||||
"abstutil",
|
||||
"anyhow",
|
||||
"geom",
|
||||
"log",
|
||||
"map_model",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "1.3.2"
|
||||
|
@ -22,6 +22,7 @@ members = [
|
||||
"popdat",
|
||||
"santa",
|
||||
"sim",
|
||||
"synthpop",
|
||||
"tests",
|
||||
"traffic_seitan",
|
||||
"traffic_signal_data",
|
||||
|
@ -267,20 +267,6 @@ impl fmt::Display for PersonID {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct OrigPersonID(
|
||||
#[serde(
|
||||
serialize_with = "serialize_usize",
|
||||
deserialize_with = "deserialize_usize"
|
||||
)]
|
||||
pub usize,
|
||||
#[serde(
|
||||
serialize_with = "serialize_usize",
|
||||
deserialize_with = "deserialize_usize"
|
||||
)]
|
||||
pub usize,
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub enum VehicleType {
|
||||
Car,
|
||||
|
@ -4,11 +4,8 @@
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use rand_xorshift::XorShiftRng;
|
||||
|
||||
pub use self::external::{ExternalPerson, ExternalTrip, ExternalTripEndpoint, MapBorders};
|
||||
pub use self::generator::{BorderSpawnOverTime, ScenarioGenerator, SpawnOverTime};
|
||||
pub use self::load::SimFlags;
|
||||
pub use self::modifier::ScenarioModifier;
|
||||
pub use self::scenario::{IndividTrip, PersonSpec, Scenario, TripPurpose};
|
||||
pub use self::spawner::TripEndpoint;
|
||||
pub(crate) use self::spawner::{StartTripArgs, TripSpec};
|
||||
|
||||
|
@ -1,114 +1,7 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_xorshift::XorShiftRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use abstio::{CityName, MapName};
|
||||
use abstutil::{prettyprint_usize, Counter, Timer};
|
||||
use geom::{Distance, Speed, Time};
|
||||
use map_model::{BuildingID, Map, OffstreetParking, RoadID};
|
||||
|
||||
use crate::make::fork_rng;
|
||||
use crate::{
|
||||
OrigPersonID, ParkingSpot, Sim, StartTripArgs, TripEndpoint, TripInfo, TripMode, Vehicle,
|
||||
VehicleSpec, VehicleType, BIKE_LENGTH, MAX_CAR_LENGTH, MIN_CAR_LENGTH,
|
||||
};
|
||||
|
||||
/// A Scenario describes all the input to a simulation. Usually a scenario covers one day.
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Scenario {
|
||||
pub scenario_name: String,
|
||||
pub map_name: MapName,
|
||||
|
||||
pub people: Vec<PersonSpec>,
|
||||
/// None means seed all buses. Otherwise the route name must be present here.
|
||||
pub only_seed_buses: Option<BTreeSet<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct PersonSpec {
|
||||
/// Just used for debugging
|
||||
pub orig_id: Option<OrigPersonID>,
|
||||
/// 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,
|
||||
pub cancelled: bool,
|
||||
/// Did a ScenarioModifier affect this?
|
||||
pub modified: bool,
|
||||
}
|
||||
|
||||
impl IndividTrip {
|
||||
pub fn new(
|
||||
depart: Time,
|
||||
purpose: TripPurpose,
|
||||
origin: TripEndpoint,
|
||||
destination: TripEndpoint,
|
||||
mode: TripMode,
|
||||
) -> IndividTrip {
|
||||
IndividTrip {
|
||||
depart,
|
||||
origin,
|
||||
destination,
|
||||
mode,
|
||||
purpose,
|
||||
cancelled: false,
|
||||
modified: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifted from Seattle's Soundcast model, but seems general enough to use anyhere.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub enum TripPurpose {
|
||||
Home,
|
||||
Work,
|
||||
School,
|
||||
Escort,
|
||||
PersonalBusiness,
|
||||
Shopping,
|
||||
Meal,
|
||||
Social,
|
||||
Recreation,
|
||||
Medical,
|
||||
ParkAndRideTransfer,
|
||||
}
|
||||
|
||||
impl fmt::Display for TripPurpose {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
TripPurpose::Home => "home",
|
||||
TripPurpose::Work => "work",
|
||||
TripPurpose::School => "school",
|
||||
// Is this like a parent escorting a child to school?
|
||||
TripPurpose::Escort => "escort",
|
||||
TripPurpose::PersonalBusiness => "personal business",
|
||||
TripPurpose::Shopping => "shopping",
|
||||
TripPurpose::Meal => "eating",
|
||||
TripPurpose::Social => "social",
|
||||
TripPurpose::Recreation => "recreation",
|
||||
TripPurpose::Medical => "medical",
|
||||
TripPurpose::ParkAndRideTransfer => "park-and-ride transfer",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Scenario {
|
||||
pub fn instantiate(&self, sim: &mut Sim, map: &Map, rng: &mut XorShiftRng, timer: &mut Timer) {
|
||||
@ -191,20 +84,89 @@ impl Scenario {
|
||||
timer.stop(format!("Instantiating {}", self.scenario_name));
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
abstio::write_binary(
|
||||
abstio::path_scenario(&self.map_name, &self.scenario_name),
|
||||
self,
|
||||
);
|
||||
}
|
||||
fn get_vehicles(
|
||||
&self,
|
||||
rng: &mut XorShiftRng,
|
||||
) -> (
|
||||
Vec<VehicleSpec>,
|
||||
Vec<(usize, BuildingID)>,
|
||||
Vec<Option<usize>>,
|
||||
) {
|
||||
let mut vehicle_specs = Vec::new();
|
||||
let mut cars_initially_parked_at = Vec::new();
|
||||
let mut vehicle_foreach_trip = Vec::new();
|
||||
|
||||
pub fn empty(map: &Map, name: &str) -> Scenario {
|
||||
Scenario {
|
||||
scenario_name: name.to_string(),
|
||||
map_name: map.get_name().clone(),
|
||||
people: Vec::new(),
|
||||
only_seed_buses: Some(BTreeSet::new()),
|
||||
let mut bike_idx = None;
|
||||
// For each indexed car, is it parked somewhere, or off-map?
|
||||
let mut car_locations: Vec<(usize, Option<BuildingID>)> = Vec::new();
|
||||
|
||||
// TODO If the trip is cancelled, this should be affected...
|
||||
for trip in &self.trips {
|
||||
let use_for_trip = match trip.mode {
|
||||
TripMode::Walk | TripMode::Transit => None,
|
||||
TripMode::Bike => {
|
||||
if bike_idx.is_none() {
|
||||
bike_idx = Some(vehicle_specs.len());
|
||||
vehicle_specs.push(Scenario::rand_bike(rng));
|
||||
}
|
||||
bike_idx
|
||||
}
|
||||
TripMode::Drive => {
|
||||
let need_parked_at = match trip.origin {
|
||||
TripEndpoint::Bldg(b) => Some(b),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Any available cars in the right spot?
|
||||
let idx = if let Some(idx) = car_locations
|
||||
.iter()
|
||||
.find(|(_, parked_at)| *parked_at == need_parked_at)
|
||||
.map(|(idx, _)| *idx)
|
||||
{
|
||||
idx
|
||||
} else {
|
||||
// Need a new car, starting in the right spot
|
||||
let idx = vehicle_specs.len();
|
||||
vehicle_specs.push(Scenario::rand_car(rng));
|
||||
if let Some(b) = need_parked_at {
|
||||
cars_initially_parked_at.push((idx, b));
|
||||
}
|
||||
idx
|
||||
};
|
||||
|
||||
// Where does this car wind up?
|
||||
car_locations.retain(|(i, _)| idx != *i);
|
||||
match trip.destination {
|
||||
TripEndpoint::Bldg(b) => {
|
||||
car_locations.push((idx, Some(b)));
|
||||
}
|
||||
TripEndpoint::Border(_) | TripEndpoint::SuddenlyAppear(_) => {
|
||||
car_locations.push((idx, None));
|
||||
}
|
||||
}
|
||||
|
||||
Some(idx)
|
||||
}
|
||||
};
|
||||
vehicle_foreach_trip.push(use_for_trip);
|
||||
}
|
||||
|
||||
// For debugging
|
||||
if false {
|
||||
let mut n = vehicle_specs.len();
|
||||
if bike_idx.is_some() {
|
||||
n -= 1;
|
||||
}
|
||||
if n > 1 {
|
||||
println!("Someone needs {} cars", n);
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
vehicle_specs,
|
||||
cars_initially_parked_at,
|
||||
vehicle_foreach_trip,
|
||||
)
|
||||
}
|
||||
|
||||
fn rand_car(rng: &mut XorShiftRng) -> VehicleSpec {
|
||||
@ -261,44 +223,6 @@ impl Scenario {
|
||||
}
|
||||
per_bldg
|
||||
}
|
||||
|
||||
pub fn remove_weird_schedules(mut self) -> Scenario {
|
||||
let orig = self.people.len();
|
||||
self.people.retain(|person| match person.check_schedule() {
|
||||
Ok(()) => true,
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
false
|
||||
}
|
||||
});
|
||||
warn!(
|
||||
"{} of {} people have nonsense schedules",
|
||||
prettyprint_usize(orig - self.people.len()),
|
||||
prettyprint_usize(orig)
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn all_trips(&self) -> impl Iterator<Item = &IndividTrip> {
|
||||
self.people.iter().flat_map(|p| p.trips.iter())
|
||||
}
|
||||
|
||||
pub fn default_scenario_for_map(name: &MapName) -> String {
|
||||
if name.city == CityName::seattle()
|
||||
&& abstio::file_exists(abstio::path_scenario(name, "weekday"))
|
||||
{
|
||||
return "weekday".to_string();
|
||||
}
|
||||
if name.city.country == "gb" {
|
||||
for x in ["background", "base_with_bg"] {
|
||||
if abstio::file_exists(abstio::path_scenario(name, x)) {
|
||||
return x.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dynamically generated -- arguably this is an absence of a default scenario
|
||||
"home_to_work".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn seed_parked_cars(
|
||||
@ -422,134 +346,110 @@ fn find_spot_near_building(
|
||||
}
|
||||
}
|
||||
|
||||
impl PersonSpec {
|
||||
/// Verify that a person's trips make sense
|
||||
fn check_schedule(&self) -> Result<()> {
|
||||
if self.trips.is_empty() {
|
||||
bail!("Person ({:?}) has no trips at all", self.orig_id);
|
||||
}
|
||||
|
||||
for pair in self.trips.windows(2) {
|
||||
if pair[0].depart >= pair[1].depart {
|
||||
bail!(
|
||||
"Person ({:?}) starts two trips in the wrong order: {} then {}",
|
||||
self.orig_id,
|
||||
pair[0].depart,
|
||||
pair[1].depart
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
impl TripEndpoint {
|
||||
/// Figure out a single PathRequest that goes between two TripEndpoints. Assume a single mode
|
||||
/// the entire time -- no walking to a car before driving, for instance. The result probably
|
||||
/// won't be exactly what would happen on a real trip between the endpoints because of this
|
||||
/// assumption.
|
||||
pub fn path_req(
|
||||
from: TripEndpoint,
|
||||
to: TripEndpoint,
|
||||
mode: TripMode,
|
||||
map: &Map,
|
||||
) -> Option<PathRequest> {
|
||||
let start = from.pos(mode, true, map)?;
|
||||
let end = to.pos(mode, false, map)?;
|
||||
Some(match mode {
|
||||
TripMode::Walk | TripMode::Transit => PathRequest::walking(start, end),
|
||||
TripMode::Bike => PathRequest::vehicle(start, end, PathConstraints::Bike),
|
||||
// Only cars leaving from a building might turn out from the driveway in a special way
|
||||
TripMode::Drive => {
|
||||
if matches!(from, TripEndpoint::Bldg(_)) {
|
||||
PathRequest::leave_from_driveway(start, end, PathConstraints::Car, map)
|
||||
} else {
|
||||
PathRequest::vehicle(start, end, PathConstraints::Car)
|
||||
}
|
||||
bail!(
|
||||
"Person ({:?}) warps from {:?} to {:?} during adjacent trips",
|
||||
self.orig_id,
|
||||
pair[0].destination,
|
||||
pair[1].origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for trip in &self.trips {
|
||||
if trip.origin == trip.destination {
|
||||
bail!(
|
||||
"Person ({:?}) has a trip from/to the same place: {:?}",
|
||||
self.orig_id,
|
||||
trip.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn get_vehicles(
|
||||
&self,
|
||||
rng: &mut XorShiftRng,
|
||||
) -> (
|
||||
Vec<VehicleSpec>,
|
||||
Vec<(usize, BuildingID)>,
|
||||
Vec<Option<usize>>,
|
||||
) {
|
||||
let mut vehicle_specs = Vec::new();
|
||||
let mut cars_initially_parked_at = Vec::new();
|
||||
let mut vehicle_foreach_trip = Vec::new();
|
||||
fn start_sidewalk_spot(&self, map: &Map) -> Result<SidewalkSpot> {
|
||||
match self {
|
||||
TripEndpoint::Bldg(b) => Ok(SidewalkSpot::building(*b, map)),
|
||||
TripEndpoint::Border(i) => SidewalkSpot::start_at_border(*i, map)
|
||||
.ok_or_else(|| anyhow!("can't start walking from {}", i)),
|
||||
TripEndpoint::SuddenlyAppear(pos) => Ok(SidewalkSpot::suddenly_appear(*pos, map)),
|
||||
}
|
||||
}
|
||||
|
||||
let mut bike_idx = None;
|
||||
// For each indexed car, is it parked somewhere, or off-map?
|
||||
let mut car_locations: Vec<(usize, Option<BuildingID>)> = Vec::new();
|
||||
fn end_sidewalk_spot(&self, map: &Map) -> Result<SidewalkSpot> {
|
||||
match self {
|
||||
TripEndpoint::Bldg(b) => Ok(SidewalkSpot::building(*b, map)),
|
||||
TripEndpoint::Border(i) => SidewalkSpot::end_at_border(*i, map)
|
||||
.ok_or_else(|| anyhow!("can't end walking at {}", i)),
|
||||
TripEndpoint::SuddenlyAppear(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO If the trip is cancelled, this should be affected...
|
||||
for trip in &self.trips {
|
||||
let use_for_trip = match trip.mode {
|
||||
TripMode::Walk | TripMode::Transit => None,
|
||||
TripMode::Bike => {
|
||||
if bike_idx.is_none() {
|
||||
bike_idx = Some(vehicle_specs.len());
|
||||
vehicle_specs.push(Scenario::rand_bike(rng));
|
||||
}
|
||||
bike_idx
|
||||
}
|
||||
TripMode::Drive => {
|
||||
let need_parked_at = match trip.origin {
|
||||
TripEndpoint::Bldg(b) => Some(b),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Any available cars in the right spot?
|
||||
let idx = if let Some(idx) = car_locations
|
||||
.iter()
|
||||
.find(|(_, parked_at)| *parked_at == need_parked_at)
|
||||
.map(|(idx, _)| *idx)
|
||||
{
|
||||
idx
|
||||
fn driving_goal(&self, constraints: PathConstraints, map: &Map) -> Result<DrivingGoal> {
|
||||
match self {
|
||||
TripEndpoint::Bldg(b) => Ok(DrivingGoal::ParkNear(*b)),
|
||||
TripEndpoint::Border(i) => map
|
||||
.get_i(*i)
|
||||
.some_incoming_road(map)
|
||||
.and_then(|dr| {
|
||||
let lanes = dr.lanes(constraints, map);
|
||||
if lanes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
// Need a new car, starting in the right spot
|
||||
let idx = vehicle_specs.len();
|
||||
vehicle_specs.push(Scenario::rand_car(rng));
|
||||
if let Some(b) = need_parked_at {
|
||||
cars_initially_parked_at.push((idx, b));
|
||||
}
|
||||
idx
|
||||
};
|
||||
// TODO ideally could use any
|
||||
Some(DrivingGoal::Border(dr.dst_i(map), lanes[0]))
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("can't end at {} for {:?}", i, constraints)),
|
||||
TripEndpoint::SuddenlyAppear(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Where does this car wind up?
|
||||
car_locations.retain(|(i, _)| idx != *i);
|
||||
match trip.destination {
|
||||
TripEndpoint::Bldg(b) => {
|
||||
car_locations.push((idx, Some(b)));
|
||||
fn pos(self, mode: TripMode, from: bool, map: &Map) -> Option<Position> {
|
||||
match mode {
|
||||
TripMode::Walk | TripMode::Transit => (if from {
|
||||
self.start_sidewalk_spot(map)
|
||||
} else {
|
||||
self.end_sidewalk_spot(map)
|
||||
})
|
||||
.ok()
|
||||
.map(|spot| spot.sidewalk_pos),
|
||||
TripMode::Drive | TripMode::Bike => {
|
||||
if from {
|
||||
match self {
|
||||
// Fall through and use DrivingGoal also to start.
|
||||
TripEndpoint::Bldg(_) => {}
|
||||
TripEndpoint::Border(i) => {
|
||||
return map.get_i(i).some_outgoing_road(map).and_then(|dr| {
|
||||
dr.lanes(mode.to_constraints(), map)
|
||||
.get(0)
|
||||
.map(|l| Position::start(*l))
|
||||
});
|
||||
}
|
||||
TripEndpoint::Border(_) | TripEndpoint::SuddenlyAppear(_) => {
|
||||
car_locations.push((idx, None));
|
||||
TripEndpoint::SuddenlyAppear(pos) => {
|
||||
return Some(pos);
|
||||
}
|
||||
}
|
||||
|
||||
Some(idx)
|
||||
}
|
||||
};
|
||||
vehicle_foreach_trip.push(use_for_trip);
|
||||
}
|
||||
|
||||
// For debugging
|
||||
if false {
|
||||
let mut n = vehicle_specs.len();
|
||||
if bike_idx.is_some() {
|
||||
n -= 1;
|
||||
}
|
||||
if n > 1 {
|
||||
println!("Someone needs {} cars", n);
|
||||
self.driving_goal(mode.to_constraints(), map)
|
||||
.ok()
|
||||
.and_then(|goal| goal.goal_pos(mode.to_constraints(), map))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
vehicle_specs,
|
||||
cars_initially_parked_at,
|
||||
vehicle_foreach_trip,
|
||||
)
|
||||
/// Returns a point representing where this endpoint is.
|
||||
pub fn pt(&self, map: &Map) -> Pt2D {
|
||||
match self {
|
||||
TripEndpoint::Bldg(b) => map.get_b(*b).polygon.center(),
|
||||
TripEndpoint::Border(i) => map.get_i(*i).polygon.center(),
|
||||
TripEndpoint::SuddenlyAppear(pos) => pos.pt(map),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -294,120 +294,3 @@ impl TripSpec {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies where a trip begins or ends.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
|
||||
pub enum TripEndpoint {
|
||||
Bldg(BuildingID),
|
||||
Border(IntersectionID),
|
||||
/// Used for interactive spawning, tests, etc. For now, only valid as a trip's start.
|
||||
SuddenlyAppear(Position),
|
||||
}
|
||||
|
||||
impl TripEndpoint {
|
||||
/// Figure out a single PathRequest that goes between two TripEndpoints. Assume a single mode
|
||||
/// the entire time -- no walking to a car before driving, for instance. The result probably
|
||||
/// won't be exactly what would happen on a real trip between the endpoints because of this
|
||||
/// assumption.
|
||||
pub fn path_req(
|
||||
from: TripEndpoint,
|
||||
to: TripEndpoint,
|
||||
mode: TripMode,
|
||||
map: &Map,
|
||||
) -> Option<PathRequest> {
|
||||
let start = from.pos(mode, true, map)?;
|
||||
let end = to.pos(mode, false, map)?;
|
||||
Some(match mode {
|
||||
TripMode::Walk | TripMode::Transit => PathRequest::walking(start, end),
|
||||
TripMode::Bike => PathRequest::vehicle(start, end, PathConstraints::Bike),
|
||||
// Only cars leaving from a building might turn out from the driveway in a special way
|
||||
TripMode::Drive => {
|
||||
if matches!(from, TripEndpoint::Bldg(_)) {
|
||||
PathRequest::leave_from_driveway(start, end, PathConstraints::Car, map)
|
||||
} else {
|
||||
PathRequest::vehicle(start, end, PathConstraints::Car)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn start_sidewalk_spot(&self, map: &Map) -> Result<SidewalkSpot> {
|
||||
match self {
|
||||
TripEndpoint::Bldg(b) => Ok(SidewalkSpot::building(*b, map)),
|
||||
TripEndpoint::Border(i) => SidewalkSpot::start_at_border(*i, map)
|
||||
.ok_or_else(|| anyhow!("can't start walking from {}", i)),
|
||||
TripEndpoint::SuddenlyAppear(pos) => Ok(SidewalkSpot::suddenly_appear(*pos, map)),
|
||||
}
|
||||
}
|
||||
|
||||
fn end_sidewalk_spot(&self, map: &Map) -> Result<SidewalkSpot> {
|
||||
match self {
|
||||
TripEndpoint::Bldg(b) => Ok(SidewalkSpot::building(*b, map)),
|
||||
TripEndpoint::Border(i) => SidewalkSpot::end_at_border(*i, map)
|
||||
.ok_or_else(|| anyhow!("can't end walking at {}", i)),
|
||||
TripEndpoint::SuddenlyAppear(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn driving_goal(&self, constraints: PathConstraints, map: &Map) -> Result<DrivingGoal> {
|
||||
match self {
|
||||
TripEndpoint::Bldg(b) => Ok(DrivingGoal::ParkNear(*b)),
|
||||
TripEndpoint::Border(i) => map
|
||||
.get_i(*i)
|
||||
.some_incoming_road(map)
|
||||
.and_then(|dr| {
|
||||
let lanes = dr.lanes(constraints, map);
|
||||
if lanes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
// TODO ideally could use any
|
||||
Some(DrivingGoal::Border(dr.dst_i(map), lanes[0]))
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("can't end at {} for {:?}", i, constraints)),
|
||||
TripEndpoint::SuddenlyAppear(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pos(self, mode: TripMode, from: bool, map: &Map) -> Option<Position> {
|
||||
match mode {
|
||||
TripMode::Walk | TripMode::Transit => (if from {
|
||||
self.start_sidewalk_spot(map)
|
||||
} else {
|
||||
self.end_sidewalk_spot(map)
|
||||
})
|
||||
.ok()
|
||||
.map(|spot| spot.sidewalk_pos),
|
||||
TripMode::Drive | TripMode::Bike => {
|
||||
if from {
|
||||
match self {
|
||||
// Fall through and use DrivingGoal also to start.
|
||||
TripEndpoint::Bldg(_) => {}
|
||||
TripEndpoint::Border(i) => {
|
||||
return map.get_i(i).some_outgoing_road(map).and_then(|dr| {
|
||||
dr.lanes(mode.to_constraints(), map)
|
||||
.get(0)
|
||||
.map(|l| Position::start(*l))
|
||||
});
|
||||
}
|
||||
TripEndpoint::SuddenlyAppear(pos) => {
|
||||
return Some(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.driving_goal(mode.to_constraints(), map)
|
||||
.ok()
|
||||
.and_then(|goal| goal.goal_pos(mode.to_constraints(), map))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a point representing where this endpoint is.
|
||||
pub fn pt(&self, map: &Map) -> Pt2D {
|
||||
match self {
|
||||
TripEndpoint::Bldg(b) => map.get_b(*b).polygon.center(),
|
||||
TripEndpoint::Border(i) => map.get_i(*i).polygon.center(),
|
||||
TripEndpoint::SuddenlyAppear(pos) => pos.pt(map),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1352,73 +1352,6 @@ pub(crate) enum TripLeg {
|
||||
RideBus(TransitRouteID, Option<TransitStopID>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord)]
|
||||
pub enum TripMode {
|
||||
Walk,
|
||||
Bike,
|
||||
Transit,
|
||||
Drive,
|
||||
}
|
||||
|
||||
impl TripMode {
|
||||
pub fn all() -> Vec<TripMode> {
|
||||
vec![
|
||||
TripMode::Walk,
|
||||
TripMode::Bike,
|
||||
TripMode::Transit,
|
||||
TripMode::Drive,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn verb(self) -> &'static str {
|
||||
match self {
|
||||
TripMode::Walk => "walk",
|
||||
TripMode::Bike => "bike",
|
||||
TripMode::Transit => "use transit",
|
||||
TripMode::Drive => "drive",
|
||||
}
|
||||
}
|
||||
|
||||
// If I used "present participle" in a method name, I'd never live it down.
|
||||
pub fn ongoing_verb(self) -> &'static str {
|
||||
match self {
|
||||
TripMode::Walk => "walking",
|
||||
TripMode::Bike => "biking",
|
||||
TripMode::Transit => "using transit",
|
||||
TripMode::Drive => "driving",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn noun(self) -> &'static str {
|
||||
match self {
|
||||
TripMode::Walk => "Pedestrian",
|
||||
TripMode::Bike => "Bike",
|
||||
TripMode::Transit => "Bus",
|
||||
TripMode::Drive => "Car",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_constraints(self) -> PathConstraints {
|
||||
match self {
|
||||
TripMode::Walk => PathConstraints::Pedestrian,
|
||||
TripMode::Bike => PathConstraints::Bike,
|
||||
// TODO WRONG
|
||||
TripMode::Transit => PathConstraints::Bus,
|
||||
TripMode::Drive => PathConstraints::Car,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_constraints(c: PathConstraints) -> TripMode {
|
||||
match c {
|
||||
PathConstraints::Pedestrian => TripMode::Walk,
|
||||
PathConstraints::Bike => TripMode::Bike,
|
||||
// TODO The bijection breaks down... transit rider vs train vs bus...
|
||||
PathConstraints::Bus | PathConstraints::Train => TripMode::Transit,
|
||||
PathConstraints::Car => TripMode::Drive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TripResult<T> {
|
||||
Ok(T),
|
||||
ModeChange,
|
||||
|
13
synthpop/Cargo.toml
Normal file
13
synthpop/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "synthpop"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
abstio = { path = "../abstio" }
|
||||
abstutil = { path = "../abstutil" }
|
||||
anyhow = "1.0.38"
|
||||
geom = { path = "../geom" }
|
||||
log = "0.4.14"
|
||||
map_model = { path = "../map_model" }
|
||||
serde = "1.0.123"
|
107
synthpop/src/lib.rs
Normal file
107
synthpop/src/lib.rs
Normal file
@ -0,0 +1,107 @@
|
||||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use abstutil::{deserialize_usize, serialize_usize};
|
||||
use map_model::{BuildingID, IntersectionID, PathConstraints, Position};
|
||||
|
||||
pub use self::external::{ExternalPerson, ExternalTrip, ExternalTripEndpoint, MapBorders};
|
||||
pub use self::modifier::ScenarioModifier;
|
||||
pub use self::scenario::{IndividTrip, PersonSpec, Scenario, TripPurpose};
|
||||
|
||||
mod external;
|
||||
mod modifier;
|
||||
mod scenario;
|
||||
|
||||
/// Specifies where a trip begins or ends.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
|
||||
pub enum TripEndpoint {
|
||||
Bldg(BuildingID),
|
||||
Border(IntersectionID),
|
||||
/// Used for interactive spawning, tests, etc. For now, only valid as a trip's start.
|
||||
SuddenlyAppear(Position),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord)]
|
||||
pub enum TripMode {
|
||||
Walk,
|
||||
Bike,
|
||||
Transit,
|
||||
Drive,
|
||||
}
|
||||
|
||||
impl TripMode {
|
||||
pub fn all() -> Vec<TripMode> {
|
||||
vec![
|
||||
TripMode::Walk,
|
||||
TripMode::Bike,
|
||||
TripMode::Transit,
|
||||
TripMode::Drive,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn verb(self) -> &'static str {
|
||||
match self {
|
||||
TripMode::Walk => "walk",
|
||||
TripMode::Bike => "bike",
|
||||
TripMode::Transit => "use transit",
|
||||
TripMode::Drive => "drive",
|
||||
}
|
||||
}
|
||||
|
||||
// If I used "present participle" in a method name, I'd never live it down.
|
||||
pub fn ongoing_verb(self) -> &'static str {
|
||||
match self {
|
||||
TripMode::Walk => "walking",
|
||||
TripMode::Bike => "biking",
|
||||
TripMode::Transit => "using transit",
|
||||
TripMode::Drive => "driving",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn noun(self) -> &'static str {
|
||||
match self {
|
||||
TripMode::Walk => "Pedestrian",
|
||||
TripMode::Bike => "Bike",
|
||||
TripMode::Transit => "Bus",
|
||||
TripMode::Drive => "Car",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_constraints(self) -> PathConstraints {
|
||||
match self {
|
||||
TripMode::Walk => PathConstraints::Pedestrian,
|
||||
TripMode::Bike => PathConstraints::Bike,
|
||||
// TODO WRONG
|
||||
TripMode::Transit => PathConstraints::Bus,
|
||||
TripMode::Drive => PathConstraints::Car,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_constraints(c: PathConstraints) -> TripMode {
|
||||
match c {
|
||||
PathConstraints::Pedestrian => TripMode::Walk,
|
||||
PathConstraints::Bike => TripMode::Bike,
|
||||
// TODO The bijection breaks down... transit rider vs train vs bus...
|
||||
PathConstraints::Bus | PathConstraints::Train => TripMode::Transit,
|
||||
PathConstraints::Car => TripMode::Drive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct OrigPersonID(
|
||||
#[serde(
|
||||
serialize_with = "serialize_usize",
|
||||
deserialize_with = "deserialize_usize"
|
||||
)]
|
||||
pub usize,
|
||||
#[serde(
|
||||
serialize_with = "serialize_usize",
|
||||
deserialize_with = "deserialize_usize"
|
||||
)]
|
||||
pub usize,
|
||||
);
|
207
synthpop/src/scenario.rs
Normal file
207
synthpop/src/scenario.rs
Normal file
@ -0,0 +1,207 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use abstio::{CityName, MapName};
|
||||
use abstutil::prettyprint_usize;
|
||||
use geom::Time;
|
||||
use map_model::Map;
|
||||
|
||||
use crate::{OrigPersonID, TripEndpoint, TripMode};
|
||||
|
||||
/// A Scenario describes all the input to a simulation. Usually a scenario covers one day.
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Scenario {
|
||||
pub scenario_name: String,
|
||||
pub map_name: MapName,
|
||||
|
||||
pub people: Vec<PersonSpec>,
|
||||
/// None means seed all buses. Otherwise the route name must be present here.
|
||||
pub only_seed_buses: Option<BTreeSet<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct PersonSpec {
|
||||
/// Just used for debugging
|
||||
pub orig_id: Option<OrigPersonID>,
|
||||
/// 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,
|
||||
pub cancelled: bool,
|
||||
/// Did a ScenarioModifier affect this?
|
||||
pub modified: bool,
|
||||
}
|
||||
|
||||
impl IndividTrip {
|
||||
pub fn new(
|
||||
depart: Time,
|
||||
purpose: TripPurpose,
|
||||
origin: TripEndpoint,
|
||||
destination: TripEndpoint,
|
||||
mode: TripMode,
|
||||
) -> IndividTrip {
|
||||
IndividTrip {
|
||||
depart,
|
||||
origin,
|
||||
destination,
|
||||
mode,
|
||||
purpose,
|
||||
cancelled: false,
|
||||
modified: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifted from Seattle's Soundcast model, but seems general enough to use anyhere.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub enum TripPurpose {
|
||||
Home,
|
||||
Work,
|
||||
School,
|
||||
Escort,
|
||||
PersonalBusiness,
|
||||
Shopping,
|
||||
Meal,
|
||||
Social,
|
||||
Recreation,
|
||||
Medical,
|
||||
ParkAndRideTransfer,
|
||||
}
|
||||
|
||||
impl fmt::Display for TripPurpose {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
TripPurpose::Home => "home",
|
||||
TripPurpose::Work => "work",
|
||||
TripPurpose::School => "school",
|
||||
// Is this like a parent escorting a child to school?
|
||||
TripPurpose::Escort => "escort",
|
||||
TripPurpose::PersonalBusiness => "personal business",
|
||||
TripPurpose::Shopping => "shopping",
|
||||
TripPurpose::Meal => "eating",
|
||||
TripPurpose::Social => "social",
|
||||
TripPurpose::Recreation => "recreation",
|
||||
TripPurpose::Medical => "medical",
|
||||
TripPurpose::ParkAndRideTransfer => "park-and-ride transfer",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Scenario {
|
||||
pub fn save(&self) {
|
||||
abstio::write_binary(
|
||||
abstio::path_scenario(&self.map_name, &self.scenario_name),
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn empty(map: &Map, name: &str) -> Scenario {
|
||||
Scenario {
|
||||
scenario_name: name.to_string(),
|
||||
map_name: map.get_name().clone(),
|
||||
people: Vec::new(),
|
||||
only_seed_buses: Some(BTreeSet::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_weird_schedules(mut self) -> Scenario {
|
||||
let orig = self.people.len();
|
||||
self.people.retain(|person| match person.check_schedule() {
|
||||
Ok(()) => true,
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
false
|
||||
}
|
||||
});
|
||||
warn!(
|
||||
"{} of {} people have nonsense schedules",
|
||||
prettyprint_usize(orig - self.people.len()),
|
||||
prettyprint_usize(orig)
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn all_trips(&self) -> impl Iterator<Item = &IndividTrip> {
|
||||
self.people.iter().flat_map(|p| p.trips.iter())
|
||||
}
|
||||
|
||||
pub fn default_scenario_for_map(name: &MapName) -> String {
|
||||
if name.city == CityName::seattle()
|
||||
&& abstio::file_exists(abstio::path_scenario(name, "weekday"))
|
||||
{
|
||||
return "weekday".to_string();
|
||||
}
|
||||
if name.city.country == "gb" {
|
||||
for x in ["background", "base_with_bg"] {
|
||||
if abstio::file_exists(abstio::path_scenario(name, x)) {
|
||||
return x.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dynamically generated -- arguably this is an absence of a default scenario
|
||||
"home_to_work".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl PersonSpec {
|
||||
/// Verify that a person's trips make sense
|
||||
fn check_schedule(&self) -> Result<()> {
|
||||
if self.trips.is_empty() {
|
||||
bail!("Person ({:?}) has no trips at all", self.orig_id);
|
||||
}
|
||||
|
||||
for pair in self.trips.windows(2) {
|
||||
if pair[0].depart >= pair[1].depart {
|
||||
bail!(
|
||||
"Person ({:?}) starts two trips in the wrong order: {} then {}",
|
||||
self.orig_id,
|
||||
pair[0].depart,
|
||||
pair[1].depart
|
||||
);
|
||||
}
|
||||
|
||||
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 ({:?}) warps from {:?} to {:?} during adjacent trips",
|
||||
self.orig_id,
|
||||
pair[0].destination,
|
||||
pair[1].origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for trip in &self.trips {
|
||||
if trip.origin == trip.destination {
|
||||
bail!(
|
||||
"Person ({:?}) has a trip from/to the same place: {:?}",
|
||||
self.orig_id,
|
||||
trip.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user