synthpop/make/
activity_model.rs

1//! An activity model creates "people" that follow a set schedule of activities through the day.
2//! Each activity (like shopping, working, sleeping) lasts some time, and requires the person to go
3//! somewhere at some time. This is an extremely simple activity model that just uses data inferred
4//! from OSM.
5
6use anyhow::Result;
7use rand::seq::SliceRandom;
8use rand::Rng;
9use rand_xorshift::XorShiftRng;
10
11use abstutil::{prettyprint_usize, Timer};
12use geom::{Distance, Duration, Time};
13use map_model::{BuildingID, BuildingType, Map, PathConstraints, PathRequest};
14
15use crate::{IndividTrip, PersonSpec, Scenario, TripEndpoint, TripMode, TripPurpose};
16
17use crate::make::{fork_rng, ScenarioGenerator};
18
19impl ScenarioGenerator {
20    /// Designed in https://github.com/a-b-street/abstreet/issues/154
21    pub fn proletariat_robot(map: &Map, rng: &mut XorShiftRng, timer: &mut Timer) -> Scenario {
22        let mut residents: Vec<BuildingID> = Vec::new();
23        let mut workers: Vec<BuildingID> = Vec::new();
24
25        let mut num_bldg_residential = 0;
26        let mut num_bldg_commercial = 0;
27        let mut num_bldg_mixed_residential_commercial = 0;
28        for b in map.all_buildings() {
29            match b.bldg_type {
30                BuildingType::Residential { num_residents, .. } => {
31                    for _ in 0..num_residents {
32                        residents.push(b.id);
33                    }
34                    num_bldg_residential += 1;
35                }
36                BuildingType::ResidentialCommercial(resident_cap, worker_cap) => {
37                    for _ in 0..resident_cap {
38                        residents.push(b.id);
39                    }
40                    for _ in 0..worker_cap {
41                        workers.push(b.id);
42                    }
43                    num_bldg_mixed_residential_commercial += 1;
44                }
45                BuildingType::Commercial(worker_cap) => {
46                    for _ in 0..worker_cap {
47                        workers.push(b.id);
48                    }
49                    num_bldg_commercial += 1;
50                }
51                BuildingType::Empty => {}
52            }
53        }
54
55        residents.shuffle(rng);
56        workers.shuffle(rng);
57
58        let mut s = Scenario::empty(map, "random people going to and from work");
59        // Include all buses/trains
60        s.only_seed_buses = None;
61
62        let residents_cap = residents.len();
63        let workers_cap = workers.len();
64
65        // this saturation figure is an arbitrary guess - we assume that the number of trips will
66        // scale as some factor of the people living and/or working on the map. A number of more
67        // than 1.0 will primarily affect the number of "pass through" trips - people who neither
68        // work nor live in the neighborhood.
69        let trip_saturation = 1.2;
70        let num_trips = (trip_saturation * (residents_cap + workers_cap) as f64) as usize;
71
72        // bound probabilities to ensure we're getting some diversity of agents
73        let lower_bound_prob = 0.05;
74        let upper_bound_prob = 0.90;
75        let prob_local_resident = if workers_cap == 0 {
76            lower_bound_prob
77        } else {
78            f64::min(
79                upper_bound_prob,
80                f64::max(lower_bound_prob, residents_cap as f64 / num_trips as f64),
81            )
82        };
83        let prob_local_worker = f64::min(
84            upper_bound_prob,
85            f64::max(lower_bound_prob, workers_cap as f64 / num_trips as f64),
86        );
87
88        debug!(
89            "BUILDINGS - workplaces: {}, residences: {}, mixed: {}",
90            prettyprint_usize(num_bldg_commercial),
91            prettyprint_usize(num_bldg_residential),
92            prettyprint_usize(num_bldg_mixed_residential_commercial)
93        );
94        debug!(
95            "CAPACITY - workers_cap: {}, residents_cap: {}, prob_local_worker: {:.1}%, \
96             prob_local_resident: {:.1}%",
97            prettyprint_usize(workers_cap),
98            prettyprint_usize(residents_cap),
99            prob_local_worker * 100.,
100            prob_local_resident * 100.
101        );
102
103        let mut num_trips_local = 0;
104        let mut num_trips_commuting_in = 0;
105        let mut num_trips_commuting_out = 0;
106        let mut num_trips_passthru = 0;
107        timer.start("create people");
108
109        // Only consider two-way intersections, so the agent can return the same way
110        // they came.
111        // TODO: instead, if it's not a two-way border, we should find an intersection
112        // an incoming border "near" the outgoing border, to allow a broader set of
113        // realistic options.
114        // TODO: prefer larger thoroughfares to better reflect reality.
115        let commuter_borders: Vec<TripEndpoint> = map
116            .all_outgoing_borders()
117            .into_iter()
118            .filter(|b| b.is_incoming_border())
119            .map(|b| TripEndpoint::Border(b.id))
120            .collect();
121        let person_params = (0..num_trips)
122            .filter_map(|_| {
123                let (is_local_resident, is_local_worker) = (
124                    rng.gen_bool(prob_local_resident),
125                    rng.gen_bool(prob_local_worker),
126                );
127                let home = if is_local_resident {
128                    if let Some(residence) = residents.pop() {
129                        TripEndpoint::Building(residence)
130                    } else {
131                        *commuter_borders.choose(rng)?
132                    }
133                } else {
134                    *commuter_borders.choose(rng)?
135                };
136
137                let work = if is_local_worker {
138                    if let Some(workplace) = workers.pop() {
139                        TripEndpoint::Building(workplace)
140                    } else {
141                        *commuter_borders.choose(rng)?
142                    }
143                } else {
144                    *commuter_borders.choose(rng)?
145                };
146
147                match (&home, &work) {
148                    (TripEndpoint::Building(_), TripEndpoint::Building(_)) => {
149                        num_trips_local += 1;
150                    }
151                    (TripEndpoint::Building(_), TripEndpoint::Border(_)) => {
152                        num_trips_commuting_out += 1;
153                    }
154                    (TripEndpoint::Border(_), TripEndpoint::Building(_)) => {
155                        num_trips_commuting_in += 1;
156                    }
157                    (TripEndpoint::Border(_), TripEndpoint::Border(_)) => {
158                        num_trips_passthru += 1;
159                    }
160                    (TripEndpoint::SuddenlyAppear(_), _) => unreachable!(),
161                    (_, TripEndpoint::SuddenlyAppear(_)) => unreachable!(),
162                };
163
164                Some((home, work, fork_rng(rng)))
165            })
166            .collect();
167
168        s.people.extend(
169            timer
170                .parallelize(
171                    "create people: making PersonSpec from endpoints",
172                    person_params,
173                    |(home, work, mut rng)| match create_prole(home, work, map, &mut rng) {
174                        Ok(person) => Some(person),
175                        Err(e) => {
176                            trace!("Unable to create person. error: {}", e);
177                            None
178                        }
179                    },
180                )
181                .into_iter()
182                .flatten(),
183        );
184
185        timer.stop("create people");
186
187        info!(
188            "TRIPS - total: {}, local: {}, commuting_in: {}, commuting_out: {}, passthru: {}, \
189             errored: {}, leftover_resident_capacity: {}, leftover_worker_capacity: {}",
190            prettyprint_usize(num_trips),
191            prettyprint_usize(num_trips_local),
192            prettyprint_usize(num_trips_commuting_in),
193            prettyprint_usize(num_trips_commuting_out),
194            prettyprint_usize(num_trips_passthru),
195            prettyprint_usize(num_trips - s.people.len()),
196            prettyprint_usize(residents.len()),
197            prettyprint_usize(workers.len()),
198        );
199        s
200    }
201}
202
203fn create_prole(
204    home: TripEndpoint,
205    work: TripEndpoint,
206    map: &Map,
207    rng: &mut XorShiftRng,
208) -> Result<PersonSpec> {
209    if home == work {
210        // TODO: handle edge-case of working and living in the same building...  maybe more likely
211        // to go for a walk later in the day or something
212        bail!("TODO: handle working and living in the same building");
213    }
214
215    let mode = match (&home, &work) {
216        // commuting entirely within map
217        (TripEndpoint::Building(home_bldg), TripEndpoint::Building(work_bldg)) => {
218            // Decide mode based on walking distance. If the buildings aren't connected,
219            // probably a bug in importing; just skip this person.
220            let dist = if let Some(path) = PathRequest::between_buildings(
221                map,
222                *home_bldg,
223                *work_bldg,
224                PathConstraints::Pedestrian,
225            )
226            .and_then(|req| map.pathfind(req).ok())
227            {
228                path.total_length()
229            } else {
230                bail!("no path found");
231            };
232
233            // TODO If home or work is in an access-restricted zone (like a living street),
234            // then probably don't drive there. Actually, it depends on the specific tagging;
235            // access=no in the US usually means a gated community.
236            select_trip_mode(dist, rng)
237        }
238        // if you exit or leave the map, we assume driving
239        _ => TripMode::Drive,
240    };
241
242    // TODO This will cause a single morning and afternoon rush. Outside of these times,
243    // it'll be really quiet. Probably want a normal distribution centered around these
244    // peak times, but with a long tail.
245    let mut depart_am = rand_time(
246        rng,
247        Time::START_OF_DAY + Duration::hours(7),
248        Time::START_OF_DAY + Duration::hours(10),
249    );
250    let mut depart_pm = rand_time(
251        rng,
252        Time::START_OF_DAY + Duration::hours(17),
253        Time::START_OF_DAY + Duration::hours(19),
254    );
255
256    if rng.gen_bool(0.1) {
257        // hacky hack to get some background traffic
258        depart_am = rand_time(
259            rng,
260            Time::START_OF_DAY + Duration::hours(0),
261            Time::START_OF_DAY + Duration::hours(12),
262        );
263        depart_pm = rand_time(
264            rng,
265            Time::START_OF_DAY + Duration::hours(12),
266            Time::START_OF_DAY + Duration::hours(24),
267        );
268    }
269
270    Ok(PersonSpec {
271        orig_id: None,
272        trips: vec![
273            IndividTrip::new(depart_am, TripPurpose::Work, home, work, mode),
274            IndividTrip::new(depart_pm, TripPurpose::Home, work, home, mode),
275        ],
276    })
277}
278
279fn select_trip_mode(distance: Distance, rng: &mut XorShiftRng) -> TripMode {
280    // TODO Make this probabilistic
281    // for example probability of walking currently has massive differences
282    // at thresholds, it would be nicer to change this gradually
283    // TODO - do not select based on distance but select one that is fastest/best in the
284    // given situation excellent bus connection / plenty of parking /
285    // cycleways / suitable rail connection all strongly influence
286    // selected mode of transport, distance is not the sole influence
287    // in some cities there may case where driving is only possible method
288    // to get somewhere, even at a short distance
289
290    // Always walk for really short trips
291    if distance < Distance::miles(0.5) {
292        return TripMode::Walk;
293    }
294
295    // Sometimes bike or walk for moderate trips
296    if distance < Distance::miles(3.0) {
297        if rng.gen_bool(0.15) {
298            return TripMode::Bike;
299        }
300        if rng.gen_bool(0.05) {
301            return TripMode::Walk;
302        }
303    }
304
305    // For longer trips, maybe bike for dedicated cyclists
306    if rng.gen_bool(0.005) {
307        return TripMode::Bike;
308    }
309    // Try transit if available, or fallback to walking
310    if rng.gen_bool(0.3) {
311        return TripMode::Transit;
312    }
313
314    // Most of the time, just drive
315    TripMode::Drive
316}
317
318fn rand_time(rng: &mut XorShiftRng, low: Time, high: Time) -> Time {
319    assert!(high > low);
320    Time::START_OF_DAY + Duration::seconds(rng.gen_range(low.inner_seconds()..high.inner_seconds()))
321}