popdat/
od.rs

1//! This is a standalone pipeline for generating a Scenario, starting from origin-destination data
2//! (also called desire lines), which gives a count of commuters between two zones, breaking down
3//! by mode.
4
5use std::collections::HashMap;
6
7use rand::seq::SliceRandom;
8use rand::Rng;
9use rand_xorshift::XorShiftRng;
10
11use abstutil::{prettyprint_usize, Timer};
12use geom::{Duration, Percent, PolyLine, Polygon, Pt2D, Time};
13use map_model::{BuildingID, BuildingType, Map};
14use synthpop::{IndividTrip, MapBorders, PersonSpec, TripEndpoint, TripMode, TripPurpose};
15
16/// This describes some number of commuters living in some named zone, working in another (or the
17/// same zone), and commuting using some mode.
18#[derive(Debug)]
19pub struct DesireLine {
20    pub home_zone: String,
21    pub work_zone: String,
22    pub mode: TripMode,
23    pub number_commuters: usize,
24}
25
26// TODO Percentage of taking a lunch trip, when to do it, how far to venture out, what mode to
27// use...
28pub struct Options {
29    /// When should somebody depart from home to work?
30    pub departure_time: NormalDistribution,
31    /// How long should somebody work before returning home?
32    pub work_duration: NormalDistribution,
33    pub include_zones: IncludeZonePolicy,
34}
35
36impl Options {
37    pub fn default() -> Options {
38        Options {
39            departure_time: NormalDistribution::new(
40                Duration::hours(8) + Duration::minutes(30),
41                Duration::minutes(30),
42            ),
43            work_duration: NormalDistribution::new(Duration::hours(9), Duration::hours(1)),
44            include_zones: IncludeZonePolicy::AllowRemote,
45        }
46    }
47}
48
49/// Only desire lines starting and ending in zones matching this policy will be used.
50#[derive(PartialEq)]
51pub enum IncludeZonePolicy {
52    /// Keep zones that at least partially overlap the map's boundary. Note this doesn't mean no
53    /// off-map trips will occur -- if a zone only partly overlaps the map, then some trips will
54    /// snap to a border.
55    MustOverlap,
56    /// Keep all zones. When looking at desire lines between two remote zones, filter by those
57    /// whose straight-line segment between zone centroids intersects the map boundary
58    AllowRemote,
59}
60
61/// Generates a scenario from aggregated origin/destination data (DesireLines). The input describes
62/// an exact number of people, who live in one zone and work in another (possibly the same) and
63/// commute using some mode. For each of them, we just need to pick a specific home and workplace
64/// from the zones, and use the Options to pick departure times. We'll wind up creating people who
65/// just take two trips daily: home -> work -> home.
66///
67/// The home and workplace may be a specific building, or they're snapped to a map border,
68/// resulting in trips that begin and/or end off-map. The amount of the zone that overlaps with the
69/// map boundary determines this. If the zone and map boundary overlap 50% by area, then half of
70/// the people to/from this zone will pick buildings, and half will pick borders.
71pub fn disaggregate(
72    map: &Map,
73    zones: HashMap<String, Polygon>,
74    desire_lines: Vec<DesireLine>,
75    opts: Options,
76    rng: &mut XorShiftRng,
77    timer: &mut Timer,
78) -> Vec<PersonSpec> {
79    // First decide which zones are relevant for our map. Match homes, shops, and border
80    // intersections to each zone.
81    let zones = create_zones(map, zones, opts.include_zones, timer);
82
83    let mut people = Vec::new();
84    let mut on_map_only = 0;
85    let mut lives_on_map = 0;
86    let mut works_on_map = 0;
87    let mut pass_through = 0;
88
89    timer.start_iter("create people per desire line", desire_lines.len());
90    for desire in desire_lines {
91        timer.next();
92        // Skip if we filtered out either zone.
93        if !zones.contains_key(&desire.home_zone) || !zones.contains_key(&desire.work_zone) {
94            continue;
95        }
96
97        let home_zone = &zones[&desire.home_zone];
98        let work_zone = &zones[&desire.work_zone];
99
100        // If both are remote, make sure the desire line intersects the map
101        if home_zone.is_remote() && work_zone.is_remote() {
102            if desire.home_zone == desire.work_zone {
103                continue;
104            }
105
106            if !map
107                .get_boundary_polygon()
108                .intersects_polyline(&PolyLine::must_new(vec![
109                    home_zone.center,
110                    work_zone.center,
111                ]))
112            {
113                continue;
114            }
115        }
116
117        for _ in 0..desire.number_commuters {
118            // Pick a specific home and workplace. It might be off-map, depending on how much the
119            // zone overlaps the map.
120            if let (Some((leave_home, goto_home)), Some((leave_work, goto_work))) = (
121                home_zone.pick_home(desire.mode, map, rng),
122                work_zone.pick_workplace(desire.mode, map, rng),
123            ) {
124                // remove_weird_schedules would clean this up later, but simpler to skip upfront
125                if leave_home == goto_work || leave_work == goto_home {
126                    continue;
127                }
128
129                match (goto_home, goto_work) {
130                    (TripEndpoint::Building(_), TripEndpoint::Building(_)) => {
131                        on_map_only += 1;
132                    }
133                    (TripEndpoint::Building(_), TripEndpoint::Border(_)) => {
134                        lives_on_map += 1;
135                    }
136                    (TripEndpoint::Border(_), TripEndpoint::Building(_)) => {
137                        works_on_map += 1;
138                    }
139                    (TripEndpoint::Border(_), TripEndpoint::Border(_)) => {
140                        pass_through += 1;
141                    }
142                    _ => unreachable!(),
143                }
144
145                // Create their schedule
146                let goto_work_time = Time::START_OF_DAY + opts.departure_time.sample(rng);
147                let return_home_time = goto_work_time + opts.work_duration.sample(rng);
148                people.push(PersonSpec {
149                    orig_id: None,
150                    trips: vec![
151                        IndividTrip::new(
152                            goto_work_time,
153                            TripPurpose::Work,
154                            leave_home,
155                            goto_work,
156                            desire.mode,
157                        ),
158                        IndividTrip::new(
159                            return_home_time,
160                            TripPurpose::Home,
161                            leave_work,
162                            goto_home,
163                            desire.mode,
164                        ),
165                    ],
166                });
167            }
168        }
169    }
170    let total = on_map_only + lives_on_map + works_on_map + pass_through;
171    for (x, label) in [
172        (on_map_only, "live and work on-map"),
173        (lives_on_map, "live on-map, work remote"),
174        (works_on_map, "live remote, work on-map"),
175        (pass_through, "just pass through"),
176    ] {
177        info!(
178            "{} people ({}) {}",
179            prettyprint_usize(x),
180            Percent::of(x, total),
181            label
182        );
183    }
184
185    people
186}
187
188struct Zone {
189    polygon: Polygon,
190    center: Pt2D,
191    pct_overlap: f64,
192    // For each building, have a value describing how many people live or work there. The exact
193    // value doesn't matter; it's just a relative weighting. This way, we can use a weighted sample
194    // and match more people to larger homes/stores.
195    homes: Vec<(BuildingID, usize)>,
196    workplaces: Vec<(BuildingID, usize)>,
197    borders: MapBorders,
198}
199
200impl Zone {
201    fn is_remote(&self) -> bool {
202        self.pct_overlap == 0.0
203    }
204}
205
206fn create_zones(
207    map: &Map,
208    input: HashMap<String, Polygon>,
209    include_zones: IncludeZonePolicy,
210    timer: &mut Timer,
211) -> HashMap<String, Zone> {
212    let all_borders = MapBorders::new(map);
213
214    let mut normal_zones = HashMap::new();
215    let mut remote_zones = HashMap::new();
216    for (name, zone) in timer
217        .parallelize(
218            "create zones",
219            input.into_iter().collect(),
220            |(name, polygon)| {
221                let mut overlapping_area = 0.0;
222                if let Ok(list) = polygon.intersection(map.get_boundary_polygon()) {
223                    for p in list {
224                        overlapping_area += p.area();
225                    }
226                }
227                // Sometimes this is slightly over 100%, because funky things happen with the polygon
228                // intersection.
229                let pct_overlap = (overlapping_area / polygon.area()).min(1.0);
230                let is_remote = pct_overlap == 0.0;
231
232                if is_remote && include_zones == IncludeZonePolicy::MustOverlap {
233                    None
234                } else {
235                    // Multiple zones might all use the same border.
236                    let center = polygon.center();
237                    let mut borders = all_borders.clone();
238                    // TODO For remote zones, we should at least prune for borders on the correct
239                    // "side" of the map. Or we can let fast_dist later take care of it.
240                    if !is_remote {
241                        for list in vec![
242                            &mut borders.incoming_walking,
243                            &mut borders.incoming_driving,
244                            &mut borders.incoming_biking,
245                            &mut borders.outgoing_walking,
246                            &mut borders.outgoing_driving,
247                            &mut borders.outgoing_biking,
248                        ] {
249                            // If the zone partly overlaps, only keep borders physically in the
250                            // zone polygon
251                            // TODO If the intersection geometry happens to leak out of the map
252                            // boundary a bit, this could be wrong!
253                            list.retain(|border| polygon.contains_pt(border.pos));
254                        }
255                    }
256                    Some((
257                        name,
258                        Zone {
259                            polygon,
260                            center,
261                            pct_overlap,
262                            homes: Vec::new(),
263                            workplaces: Vec::new(),
264                            borders,
265                        },
266                    ))
267                }
268            },
269        )
270        .into_iter()
271        .flatten()
272    {
273        if zone.is_remote() {
274            remote_zones.insert(name, zone);
275        } else {
276            normal_zones.insert(name, zone);
277        }
278    }
279
280    info!(
281        "{} zones partly in the map boundary, {} remote zones",
282        prettyprint_usize(normal_zones.len()),
283        prettyprint_usize(remote_zones.len())
284    );
285
286    // Match all buildings to a normal zone.
287    timer.start_iter("assign buildings to zones", map.all_buildings().len());
288    for b in map.all_buildings() {
289        timer.next();
290        let center = b.polygon.center();
291        // We're assuming zones don't overlap each other, so just look for the first match.
292        if let Some((_, zone)) = normal_zones
293            .iter_mut()
294            .find(|(_, z)| z.polygon.contains_pt(center))
295        {
296            match b.bldg_type {
297                // The current heuristics for num_residents sometimes assign 0 people to a
298                // building. We never want that, so just scale them all up.
299                BuildingType::Residential { num_residents, .. } => {
300                    zone.homes.push((b.id, num_residents + 1));
301                }
302                BuildingType::ResidentialCommercial(num_residents, _) => {
303                    zone.homes.push((b.id, num_residents + 1));
304                    // We know how many different stores are located in each building, according to
305                    // OSM. A big mall might have 10 amenities, while standalone
306                    // shops just have 1.
307                    zone.workplaces.push((b.id, b.amenities.len()));
308                }
309                BuildingType::Commercial(_) => {
310                    zone.workplaces.push((b.id, b.amenities.len()));
311                }
312                BuildingType::Empty => {}
313            }
314        }
315    }
316
317    normal_zones.extend(remote_zones);
318    normal_zones
319}
320
321impl Zone {
322    /// Returns endpoints to (leave home, goto home). These're usually the same, except in some
323    /// cases of border trips using divided one-ways.
324    fn pick_home(
325        &self,
326        mode: TripMode,
327        map: &Map,
328        rng: &mut XorShiftRng,
329    ) -> Option<(TripEndpoint, TripEndpoint)> {
330        if rng.gen_bool(self.pct_overlap) && !self.homes.is_empty() {
331            let b = self.homes.choose_weighted(rng, |(_, n)| *n).unwrap().0;
332            return Some((TripEndpoint::Building(b), TripEndpoint::Building(b)));
333        }
334        self.pick_borders(mode, map, rng)
335    }
336
337    /// Returns endpoints to (leave work, goto work). These're usually the same, except in some
338    /// cases of border trips using divided one-ways.
339    fn pick_workplace(
340        &self,
341        mode: TripMode,
342        map: &Map,
343        rng: &mut XorShiftRng,
344    ) -> Option<(TripEndpoint, TripEndpoint)> {
345        if rng.gen_bool(self.pct_overlap) && !self.workplaces.is_empty() {
346            let b = self.workplaces.choose_weighted(rng, |(_, n)| *n).unwrap().0;
347            return Some((TripEndpoint::Building(b), TripEndpoint::Building(b)));
348        }
349        self.pick_borders(mode, map, rng)
350    }
351
352    fn pick_borders(
353        &self,
354        mode: TripMode,
355        map: &Map,
356        rng: &mut XorShiftRng,
357    ) -> Option<(TripEndpoint, TripEndpoint)> {
358        let (incoming, outgoing) = self.borders.for_mode(mode);
359
360        let leave_i = incoming
361            .choose_weighted(rng, |border| {
362                (border.weight as f64) * self.center.fast_dist(border.pos).into_inner()
363            })
364            .ok()?
365            .i;
366
367        // If we can use the same border on the way back, prefer that.
368        if outgoing.iter().any(|border| border.i == leave_i) {
369            return Some((TripEndpoint::Border(leave_i), TripEndpoint::Border(leave_i)));
370        }
371        // Otherwise, we might have to use a separate border to re-enter. Prefer the one closest to
372        // the first, to have a better chance of matching up divided one-ways.
373        let leave_pt = map.get_i(leave_i).polygon.center();
374        let goto_i = outgoing
375            .iter()
376            .min_by_key(|border| map.get_i(border.i).polygon.center().dist_to(leave_pt))?
377            .i;
378        Some((TripEndpoint::Border(leave_i), TripEndpoint::Border(goto_i)))
379    }
380}
381
382/// A normal distribution of Durations.
383pub struct NormalDistribution {
384    pub mean: Duration,
385    pub std_deviation: Duration,
386}
387
388impl NormalDistribution {
389    pub fn new(mean: Duration, std_deviation: Duration) -> NormalDistribution {
390        NormalDistribution {
391            mean,
392            std_deviation,
393        }
394    }
395
396    pub fn sample(&self, rng: &mut XorShiftRng) -> Duration {
397        use rand_distr::{Distribution, Normal};
398
399        Duration::seconds(
400            Normal::new(
401                self.mean.inner_seconds(),
402                self.std_deviation.inner_seconds(),
403            )
404            .unwrap()
405            .sample(rng),
406        )
407    }
408}