popdat/
make_person.rs

1use std::collections::HashMap;
2
3use rand::seq::SliceRandom;
4use rand::Rng;
5use rand_xorshift::XorShiftRng;
6
7use abstutil::Timer;
8use map_model::{BuildingID, IntersectionID, Map, PathConstraints, PathRequest};
9use synthpop::{IndividTrip, PersonSpec, TripEndpoint, TripMode, TripPurpose};
10
11use crate::{Activity, CensusPerson, Config};
12
13pub fn make_people(
14    people: Vec<CensusPerson>,
15    map: &Map,
16    timer: &mut Timer,
17    rng: &mut XorShiftRng,
18    config: &Config,
19) -> Vec<PersonSpec> {
20    // Only consider two-way intersections, so the agent can return the same way
21    // they came.
22    // TODO: instead, if it's not a two-way border, we should find an intersection
23    // an incoming border "near" the outgoing border, to allow a broader set of
24    // realistic options.
25    // TODO: prefer larger thoroughfares to better reflect reality.
26    let commuter_borders: Vec<IntersectionID> = map
27        .all_outgoing_borders()
28        .into_iter()
29        .filter(|b| b.is_incoming_border())
30        .map(|b| b.id)
31        .collect();
32
33    // TODO Where should we validate that at least one border exists? Probably in
34    // generate_scenario, at minimum.
35
36    let person_factory = PersonFactory::new(map);
37    let make_person_inputs = people
38        .into_iter()
39        .map(|person| (person, sim::fork_rng(rng)))
40        .collect();
41    timer.parallelize(
42        "making people in parallel",
43        make_person_inputs,
44        |(person, mut rng)| {
45            person_factory.make_person(person, map, &commuter_borders, &mut rng, config)
46        },
47    )
48}
49
50struct PersonFactory {
51    activity_to_buildings: HashMap<Activity, Vec<BuildingID>>,
52}
53
54impl PersonFactory {
55    fn new(map: &Map) -> Self {
56        let activity_to_buildings = Self::activity_to_buildings(map);
57        Self {
58            activity_to_buildings,
59        }
60    }
61
62    fn activity_to_buildings(map: &Map) -> HashMap<Activity, Vec<BuildingID>> {
63        // What types of OpenStreetMap amenities will satisfy each activity?
64        let categories = vec![
65            (Activity::Breakfast, vec!["cafe"]),
66            (Activity::Lunch, vec!["pub", "food_court", "fast_food"]),
67            (
68                Activity::Dinner,
69                vec!["restaurant", "theatre", "biergarten"],
70            ),
71            (
72                Activity::School,
73                vec![
74                    "college",
75                    "kindergarten",
76                    "language_school",
77                    "library",
78                    "music_school",
79                    "university",
80                ],
81            ),
82            (
83                Activity::Entertainment,
84                vec![
85                    "arts_centre",
86                    "casino",
87                    "cinema",
88                    "community_centre",
89                    "fountain",
90                    "gambling",
91                    "nightclub",
92                    "planetarium",
93                    "public_bookcase",
94                    "pool",
95                    "dojo",
96                    "social_centre",
97                    "social_centre",
98                    "studio",
99                    "theatre",
100                    "bar",
101                    "bbq",
102                    "bicycle_rental",
103                    "boat_rental",
104                    "boat_sharing",
105                    "dive_centre",
106                    "internet_cafe",
107                ],
108            ),
109            (
110                Activity::Errands,
111                vec![
112                    "marketplace",
113                    "post_box",
114                    "photo_booth",
115                    "recycling",
116                    "townhall",
117                ],
118            ),
119            (Activity::Financial, vec!["bank", "atm", "bureau_de_change"]),
120            (
121                Activity::Healthcare,
122                vec![
123                    "baby_hatch",
124                    "clinic",
125                    "dentist",
126                    "doctors",
127                    "hospital",
128                    "nursing_home",
129                    "pharmacy",
130                    "social_facility",
131                    "veterinary",
132                    "childcare",
133                ],
134            ),
135            (Activity::Work, vec!["bank", "clinic"]),
136        ];
137
138        // Find all buildings with a matching amenity
139        let mut candidates: HashMap<Activity, Vec<BuildingID>> = HashMap::new();
140        for b in map.all_buildings() {
141            for (activity, categories) in &categories {
142                for amenity in &b.amenities {
143                    if categories.contains(&amenity.amenity_type.as_str()) {
144                        candidates
145                            .entry(*activity)
146                            .and_modify(|v| v.push(b.id))
147                            .or_insert_with(|| vec![b.id]);
148                    }
149                }
150            }
151        }
152        candidates
153    }
154
155    fn find_building_for_activity(
156        &self,
157        activity: Activity,
158        _start: TripEndpoint,
159        _map: &Map,
160        rng: &mut XorShiftRng,
161    ) -> Option<BuildingID> {
162        // TODO If there are several choices of building that satisfy an activity, which one will
163        // someone choose? One simple approach could just calculate the difficulty of going from the
164        // previous location (starting from home) to that place, using some mode of travel. Then
165        // either pick the closest choice, or even better, randomize, but weight based on
166        // the cost of getting there. map.pathfind() may be helpful.
167
168        // For now, just pick a random one
169        self.activity_to_buildings
170            .get(&activity)
171            .and_then(|buildings| buildings.choose(rng).cloned())
172    }
173
174    pub fn make_person(
175        &self,
176        person: CensusPerson,
177        map: &Map,
178        commuter_borders: &[IntersectionID],
179        rng: &mut XorShiftRng,
180        config: &Config,
181    ) -> PersonSpec {
182        let schedule = person.generate_schedule(config, rng);
183
184        let mut output = PersonSpec {
185            orig_id: None,
186            trips: Vec::new(),
187        };
188
189        let mut current_location = TripEndpoint::Building(person.home);
190        for (departure_time, activity) in schedule.activities {
191            // TODO This field isn't that important; later we could map Activity to a TripPurpose
192            // better.
193            let purpose = TripPurpose::Shopping;
194
195            let goto = if let Some(destination) =
196                self.find_building_for_activity(activity, current_location, map, rng)
197            {
198                TripEndpoint::Building(destination)
199            } else if let Some(i) = commuter_borders.choose(rng) {
200                // No buildings satisfy the activity. Just go somewhere off-map.
201                TripEndpoint::Border(*i)
202            } else {
203                // Broken map without borders. Don't crash, just skip the person
204                continue;
205            };
206
207            let mode = pick_mode(current_location, goto, map, rng, config);
208            output.trips.push(IndividTrip::new(
209                departure_time,
210                purpose,
211                current_location,
212                goto,
213                mode,
214            ));
215
216            current_location = goto;
217        }
218
219        output
220    }
221}
222
223fn pick_mode(
224    from: TripEndpoint,
225    to: TripEndpoint,
226    map: &Map,
227    rng: &mut XorShiftRng,
228    config: &Config,
229) -> TripMode {
230    let (b1, b2) = match (from, to) {
231        (TripEndpoint::Building(b1), TripEndpoint::Building(b2)) => (b1, b2),
232        // TODO Always drive when going on or off-map?
233        _ => {
234            return TripMode::Drive;
235        }
236    };
237
238    // Decide mode based on walking distance
239    let distance = if let Some(path) =
240        PathRequest::between_buildings(map, b1, b2, PathConstraints::Pedestrian)
241            .and_then(|req| map.pathfind(req).ok())
242    {
243        path.total_length()
244    } else {
245        // If the buildings aren't connected, there was probably a bug importing the map. Just
246        // fallback to driving. If the trip can't be started in the simulation, it'll show up as
247        // cancelled with more details about the problem.
248        return TripMode::Drive;
249    };
250
251    // TODO If either endpoint is in an access-restricted zone (like a living street), then
252    // probably don't drive there. Actually, it depends on the specific tagging; access=no in the
253    // US usually means a gated community.
254
255    // TODO Make this probabilistic
256    // for example probability of walking currently has massive differences
257    // at thresholds, it would be nicer to change this gradually
258    // TODO - do not select based on distance but select one that is fastest/best in the
259    // given situation excellent bus connection / plenty of parking /
260    // cycleways / suitable rail connection all strongly influence
261    // selected mode of transport, distance is not the sole influence
262    // in some cities there may case where driving is only possible method
263    // to get somewhere, even at a short distance
264
265    // Always walk for really short trips
266    if distance < config.walk_for_distances_shorter_than {
267        return TripMode::Walk;
268    }
269
270    // Sometimes bike or walk for moderate trips
271    if distance < config.walk_or_bike_for_distances_shorter_than {
272        // TODO We could move all of these params to Config, but I'm not sure if the overall flow
273        // of logic in this function is what we want yet.
274        if rng.gen_bool(0.15) {
275            return TripMode::Bike;
276        }
277        if rng.gen_bool(0.05) {
278            return TripMode::Walk;
279        }
280    }
281
282    // For longer trips, maybe bike for dedicated cyclists
283    if rng.gen_bool(0.005) {
284        return TripMode::Bike;
285    }
286    // Try transit if available, or fallback to walking
287    if rng.gen_bool(0.3) {
288        return TripMode::Transit;
289    }
290
291    // Most of the time, just drive
292    TripMode::Drive
293}