synthpop/
scenario.rs

1use std::collections::BTreeSet;
2use std::fmt;
3
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6
7use abstio::{CityName, MapName};
8use abstutil::prettyprint_usize;
9use geom::Time;
10use map_model::Map;
11
12use crate::{OrigPersonID, TripEndpoint, TripMode};
13
14/// A Scenario describes all the input to a simulation. Usually a scenario covers one day.
15#[derive(Clone, Serialize, Deserialize, Debug)]
16pub struct Scenario {
17    pub scenario_name: String,
18    pub map_name: MapName,
19
20    pub people: Vec<PersonSpec>,
21    /// None means seed all buses. Otherwise the route name must be present here.
22    pub only_seed_buses: Option<BTreeSet<String>>,
23}
24
25#[derive(Clone, Serialize, Deserialize, Debug)]
26pub struct PersonSpec {
27    /// Just used for debugging
28    pub orig_id: Option<OrigPersonID>,
29    /// There must be continuity between trips: each trip starts at the destination of the previous
30    /// trip. In the case of borders, the outbound and inbound border may be different. This means
31    /// that there was some sort of "remote" trip happening outside the map that we don't simulate.
32    pub trips: Vec<IndividTrip>,
33}
34
35#[derive(Clone, Serialize, Deserialize, Debug)]
36pub struct IndividTrip {
37    pub depart: Time,
38    pub origin: TripEndpoint,
39    pub destination: TripEndpoint,
40    pub mode: TripMode,
41    pub purpose: TripPurpose,
42    pub cancelled: bool,
43    /// Did a ScenarioModifier affect this?
44    pub modified: bool,
45}
46
47impl IndividTrip {
48    pub fn new(
49        depart: Time,
50        purpose: TripPurpose,
51        origin: TripEndpoint,
52        destination: TripEndpoint,
53        mode: TripMode,
54    ) -> IndividTrip {
55        IndividTrip {
56            depart,
57            origin,
58            destination,
59            mode,
60            purpose,
61            cancelled: false,
62            modified: false,
63        }
64    }
65}
66
67/// Lifted from Seattle's Soundcast model, but seems general enough to use anyhere.
68#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
69pub enum TripPurpose {
70    Home,
71    Work,
72    School,
73    Escort,
74    PersonalBusiness,
75    Shopping,
76    Meal,
77    Social,
78    Recreation,
79    Medical,
80    ParkAndRideTransfer,
81}
82
83impl fmt::Display for TripPurpose {
84    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
85        write!(
86            f,
87            "{}",
88            match self {
89                TripPurpose::Home => "home",
90                TripPurpose::Work => "work",
91                TripPurpose::School => "school",
92                // Is this like a parent escorting a child to school?
93                TripPurpose::Escort => "escort",
94                TripPurpose::PersonalBusiness => "personal business",
95                TripPurpose::Shopping => "shopping",
96                TripPurpose::Meal => "eating",
97                TripPurpose::Social => "social",
98                TripPurpose::Recreation => "recreation",
99                TripPurpose::Medical => "medical",
100                TripPurpose::ParkAndRideTransfer => "park-and-ride transfer",
101            }
102        )
103    }
104}
105
106impl Scenario {
107    pub fn save(&self) {
108        abstio::write_binary(
109            abstio::path_scenario(&self.map_name, &self.scenario_name),
110            self,
111        );
112    }
113
114    pub fn empty(map: &Map, name: &str) -> Scenario {
115        Scenario {
116            scenario_name: name.to_string(),
117            map_name: map.get_name().clone(),
118            people: Vec::new(),
119            only_seed_buses: Some(BTreeSet::new()),
120        }
121    }
122
123    pub fn remove_weird_schedules(mut self, verbose: bool) -> Scenario {
124        let orig = self.people.len();
125        self.people.retain(|person| match person.check_schedule() {
126            Ok(()) => true,
127            Err(err) => {
128                if verbose {
129                    warn!("{}", err);
130                }
131                false
132            }
133        });
134        warn!(
135            "{} of {} people have nonsense schedules",
136            prettyprint_usize(orig - self.people.len()),
137            prettyprint_usize(orig)
138        );
139        self
140    }
141
142    pub fn all_trips(&self) -> impl Iterator<Item = &IndividTrip> {
143        self.people.iter().flat_map(|p| p.trips.iter())
144    }
145
146    pub fn default_scenario_for_map(name: &MapName) -> String {
147        if name.city == CityName::seattle()
148            && abstio::file_exists(abstio::path_scenario(name, "weekday"))
149        {
150            return "weekday".to_string();
151        }
152        if name.city.country == "gb" {
153            for x in ["background", "base_with_bg"] {
154                if abstio::file_exists(abstio::path_scenario(name, x)) {
155                    return x.to_string();
156                }
157            }
158        }
159        // Dynamically generated -- arguably this is an absence of a default scenario
160        "home_to_work".to_string()
161    }
162}
163
164impl PersonSpec {
165    /// Verify that a person's trips make sense
166    pub fn check_schedule(&self) -> Result<()> {
167        if self.trips.is_empty() {
168            bail!("Person ({:?}) has no trips at all", self.orig_id);
169        }
170
171        for pair in self.trips.windows(2) {
172            if pair[0].depart >= pair[1].depart {
173                bail!(
174                    "Person ({:?}) starts two trips in the wrong order: {} then {}",
175                    self.orig_id,
176                    pair[0].depart,
177                    pair[1].depart
178                );
179            }
180
181            if pair[0].destination != pair[1].origin {
182                // Exiting one border and re-entering another is fine
183                if matches!(pair[0].destination, TripEndpoint::Border(_))
184                    && matches!(pair[1].origin, TripEndpoint::Border(_))
185                {
186                    continue;
187                }
188                bail!(
189                    "Person ({:?}) warps from {:?} to {:?} during adjacent trips",
190                    self.orig_id,
191                    pair[0].destination,
192                    pair[1].origin
193                );
194            }
195        }
196
197        for trip in &self.trips {
198            if trip.origin == trip.destination {
199                bail!(
200                    "Person ({:?}) has a trip from/to the same place: {:?}",
201                    self.orig_id,
202                    trip.origin
203                );
204            }
205        }
206
207        Ok(())
208    }
209}