Skip to content

Commit 5afbd27

Browse files
committed
Add a representation of zone-based OD data. Use it for impact mode if
available. Start adapting code from NPW to build demand models for Scotland. Just assemble inputs. Generate the Scottish demand models in a brute-force way. Still only 70s though. Make the web app load and plumb along the demand model Store the demand model, and start a mode for the user to debug it, just showing the zones Change the serialized zone format, and start plumbing back desire line counts for debugging
1 parent e64c656 commit 5afbd27

25 files changed

+1662
-52
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
target/
33
node_modules/
44
data_prep/scotland/tmp
5+
data_prep/scotland/demand
56
# Local symlinks to avoid hitting od2net.org
67
web/public/osm
78
web/public/boundaries
89
web/public/cnt_osm
910
web/public/cnt_boundaries
11+
web/public/cnt_demand

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ automatically use the latest OSM data. These are manually managed and hosted by
3434
Dustin on assets.od2net.org.
3535

3636
If you're developing locally, you can avoid hitting od2net.org by setting up
37-
two directories in `web/public/`: `osm` and `boundaries`. If
37+
three directories in `web/public/`: `osm`, `boundaries`, and `cnt_demand`. If
3838
`web/public/osm/areas.json` exists, then the Svelte app will load from
3939
localhost, not od2net.org. You can copy from od2net.org to set this up,
4040
choosing what study areas to cache:
@@ -43,7 +43,7 @@ choosing what study areas to cache:
4343
AREAS="bristol edinburgh strasbourg ut_dallas"
4444
4545
cd web/public
46-
mkdir boundaries osm
46+
mkdir boundaries osm cnt_demand
4747
4848
cd osm
4949
wget https://assets.od2net.org/severance_pbfs/areas.json
@@ -58,17 +58,19 @@ done
5858
cd ..
5959
```
6060

61-
There are two more directories particular to Scotland. To cache all of that data:
61+
There are three more directories particular to Scotland. To cache all of that data:
6262

6363
```
6464
# Still in web/public
6565
66-
mkdir cnt_osm cnt_boundaries
66+
mkdir cnt_osm cnt_boundaries cnt_demand
6767
jq '.features[] | .properties.kind + "_" + .properties.name' ../../data_prep/scotland/boundaries.geojson | sed 's/"//g' | while read x; do
6868
wget https://assets.od2net.org/cnt_boundaries/$x.geojson
6969
wget https://assets.od2net.org/cnt_osm/$x.osm.pbf
70+
wget https://assets.od2net.org/cnt_demand/$x.bin
7071
mv $x.geojson cnt_boundaries
7172
mv $x.osm.pbf cnt_osm
73+
mv $x.bin cnt_demand
7274
done
7375
```
7476

backend/src/create.rs

+11-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use utils::{
1111
};
1212

1313
use crate::{
14-
impact::Impact, Direction, FilterKind, Intersection, IntersectionID, MapModel, Road, RoadID,
15-
Router,
14+
impact::Impact, od::DemandModel, Direction, FilterKind, Intersection, IntersectionID, MapModel,
15+
Road, RoadID, Router,
1616
};
1717

1818
#[derive(Default)]
@@ -132,6 +132,7 @@ pub fn create_from_osm(
132132
input_bytes: &[u8],
133133
boundary_wgs84: MultiPolygon,
134134
study_area_name: Option<String>,
135+
demand: Option<DemandModel>,
135136
) -> Result<MapModel> {
136137
let mut osm = Osm::default();
137138
let mut graph = Graph::new(input_bytes, is_road, &mut osm)?;
@@ -212,12 +213,19 @@ pub fn create_from_osm(
212213
directions,
213214

214215
impact: None,
216+
demand: None,
215217

216218
undo_stack: Vec::new(),
217219
redo_queue: Vec::new(),
218220
boundaries: BTreeMap::new(),
219221
};
220-
map.impact = Some(Impact::new(&map));
222+
if let Some(mut demand) = demand {
223+
demand.finish_loading(&map.mercator);
224+
map.impact = Some(Impact::new(&map, Some(&demand)));
225+
map.demand = Some(demand);
226+
} else {
227+
map.impact = Some(Impact::new(&map, None));
228+
}
221229

222230
let graph = GraphSubset {
223231
node_to_edge: graph.node_to_edge,

backend/src/impact.rs

+9-20
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
use std::collections::HashMap;
22

33
use geojson::{Feature, FeatureCollection};
4-
use nanorand::{Rng, WyRand};
54

6-
use crate::{IntersectionID, MapModel, RoadID};
5+
use crate::{od, IntersectionID, MapModel, RoadID};
76

87
// TODO Rename?
98
/// Besides just studying the impact on shortcuts within one neighbourhood boundary, the user can
109
/// see how traffic changes across roads in the whole map. This works by finding the best route
1110
/// before and after changes for every origin/destination "OD" pairs, then counting routes per
1211
/// road.
1312
pub struct Impact {
14-
requests: Vec<(IntersectionID, IntersectionID)>,
13+
// (i1, i2, count) -- `count` identical trips from `i1` to `i2`
14+
requests: Vec<(IntersectionID, IntersectionID, usize)>,
1515

1616
// TODO Can use Vec for perf
1717
counts_before: HashMap<RoadID, usize>,
@@ -20,9 +20,12 @@ pub struct Impact {
2020

2121
impl Impact {
2222
/// Calculates `requests` only
23-
pub fn new(map: &MapModel) -> Self {
23+
pub fn new(map: &MapModel, demand: Option<&od::DemandModel>) -> Self {
2424
Self {
25-
requests: synthetic_od_requests(map),
25+
requests: match demand {
26+
Some(demand) => demand.make_requests(map),
27+
None => od::synthetic_od_requests(map),
28+
},
2629
counts_before: HashMap::new(),
2730
counts_after: HashMap::new(),
2831
}
@@ -91,7 +94,7 @@ impl Impact {
9194
let router_after = map.router_after.as_ref().unwrap();
9295

9396
// TODO We could remember the indices of requests that have changes
94-
for (i1, i2) in &self.requests {
97+
for (i1, i2, _) in &self.requests {
9598
let Some(route1) = router_before.route_from_intersections(map, *i1, *i2) else {
9699
continue;
97100
};
@@ -110,17 +113,3 @@ impl Impact {
110113
changed_paths
111114
}
112115
}
113-
114-
/// Deterministically produce a bunch of OD pairs, just as a fallback when there's no real data
115-
fn synthetic_od_requests(map: &MapModel) -> Vec<(IntersectionID, IntersectionID)> {
116-
let num_requests = 1_000;
117-
118-
let mut rng = WyRand::new_seed(42);
119-
let mut requests = Vec::new();
120-
for _ in 0..num_requests {
121-
let i1 = IntersectionID(rng.generate_range(0..map.intersections.len()));
122-
let i2 = IntersectionID(rng.generate_range(0..map.intersections.len()));
123-
requests.push((i1, i2));
124-
}
125-
requests
126-
}

backend/src/lib.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod impact;
2727
mod map_model;
2828
mod movements;
2929
mod neighbourhood;
30+
pub mod od;
3031
mod render_cells;
3132
mod route;
3233
mod route_snapper;
@@ -51,6 +52,8 @@ impl LTN {
5152
#[wasm_bindgen(constructor)]
5253
pub fn new(
5354
input_bytes: &[u8],
55+
// Option doesn't work; the caller should just pass in 0 bytes to mean empty
56+
demand_bytes: &[u8],
5457
boundary_input: JsValue,
5558
study_area_name: Option<String>,
5659
) -> Result<LTN, JsValue> {
@@ -70,7 +73,12 @@ impl LTN {
7073
}
7174
};
7275

73-
let map = MapModel::new(input_bytes, multi_polygon, study_area_name).map_err(err_to_js)?;
76+
let mut demand = None;
77+
if demand_bytes.len() > 0 {
78+
demand = Some(bincode::deserialize(demand_bytes).map_err(err_to_js)?);
79+
}
80+
let map = MapModel::new(input_bytes, multi_polygon, study_area_name, demand)
81+
.map_err(err_to_js)?;
7482
Ok(LTN {
7583
map,
7684
neighbourhood: None,
@@ -395,6 +403,14 @@ impl LTN {
395403
)
396404
}
397405

406+
#[wasm_bindgen(js_name = getDemandModel)]
407+
pub fn get_demand_model(&self) -> Result<String, JsValue> {
408+
let Some(ref demand) = self.map.demand else {
409+
return Err(JsValue::from_str("no demand model"));
410+
};
411+
Ok(serde_json::to_string(&demand.to_gj(&self.map)).map_err(err_to_js)?)
412+
}
413+
398414
// TODO This is also internal to MapModel. But not sure who should own Neighbourhood or how to
399415
// plumb, so duplicting here.
400416
fn after_edit(&mut self) {

backend/src/map_model.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::geo_helpers::{
66
invert_multi_polygon, limit_angle, linestring_intersection,
77
};
88
use crate::impact::Impact;
9-
use crate::Router;
9+
use crate::{od::DemandModel, Router};
1010
use anyhow::Result;
1111
use geo::{
1212
Closest, ClosestPoint, Coord, Distance, Euclidean, Length, LineInterpolatePoint,
@@ -49,6 +49,7 @@ pub struct MapModel {
4949
pub directions: BTreeMap<RoadID, Direction>,
5050

5151
pub impact: Option<Impact>,
52+
pub demand: Option<DemandModel>,
5253

5354
// TODO Keep edits / state here or not?
5455
pub undo_stack: Vec<Command>,
@@ -134,8 +135,9 @@ impl MapModel {
134135
input_bytes: &[u8],
135136
boundary_wgs84: MultiPolygon,
136137
study_area_name: Option<String>,
138+
demand: Option<DemandModel>,
137139
) -> Result<MapModel> {
138-
crate::create::create_from_osm(input_bytes, boundary_wgs84, study_area_name)
140+
crate::create::create_from_osm(input_bytes, boundary_wgs84, study_area_name, demand)
139141
}
140142

141143
pub fn get_r(&self, r: RoadID) -> &Road {

backend/src/od.rs

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use geo::{BoundingRect, Contains, MultiPolygon, Point};
2+
use geojson::GeoJson;
3+
use nanorand::{Rng, WyRand};
4+
use serde::{Deserialize, Serialize};
5+
use utils::Mercator;
6+
7+
use crate::{IntersectionID, MapModel};
8+
9+
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
10+
pub struct ZoneID(pub usize);
11+
12+
/// Origin/destination demand data, representing driving trips between two zones.
13+
#[derive(Serialize, Deserialize)]
14+
pub struct DemandModel {
15+
pub zones: Vec<Zone>,
16+
// (zone1, zone2, count), with count being the number of trips from zone1 to zone2
17+
pub desire_lines: Vec<(ZoneID, ZoneID, usize)>,
18+
}
19+
20+
impl DemandModel {
21+
/// Turn all of the zones into Mercator. Don't do this when originally building and serializing
22+
/// them, because that process might not use exactly the same Mercator object.
23+
pub fn finish_loading(&mut self, mercator: &Mercator) {
24+
for zone in &mut self.zones {
25+
mercator.to_mercator_in_place(&mut zone.geometry);
26+
let bbox = zone.geometry.bounding_rect().unwrap();
27+
zone.x1 = (bbox.min().x * 100.0) as i64;
28+
zone.y1 = (bbox.min().y * 100.0) as i64;
29+
zone.x2 = (bbox.max().x * 100.0) as i64;
30+
zone.y2 = (bbox.max().y * 100.0) as i64;
31+
}
32+
}
33+
34+
pub fn make_requests(&self, map: &MapModel) -> Vec<(IntersectionID, IntersectionID, usize)> {
35+
info!(
36+
"Making requests from {} zones and {} desire lines",
37+
self.zones.len(),
38+
self.desire_lines.len()
39+
);
40+
41+
// TODO Plumb through UI
42+
// To speed up the impact calculation, how many specific requests per (zone1, zone2)? If
43+
// true, just do one, but weight it by count.
44+
let fast_sample = true;
45+
46+
let mut rng = WyRand::new_seed(42);
47+
let mut requests = Vec::new();
48+
49+
for (zone1, zone2, raw_count) in &self.desire_lines {
50+
let (iterations, trip_count) = if fast_sample {
51+
(1, *raw_count)
52+
} else {
53+
(*raw_count, 1)
54+
};
55+
56+
for _ in 0..iterations {
57+
let pt1 = self.zones[zone1.0].random_point(&mut rng);
58+
let pt2 = self.zones[zone2.0].random_point(&mut rng);
59+
if let (Some(i1), Some(i2)) = (
60+
map.closest_intersection
61+
.nearest_neighbor(&pt1)
62+
.map(|obj| obj.data),
63+
map.closest_intersection
64+
.nearest_neighbor(&pt2)
65+
.map(|obj| obj.data),
66+
) {
67+
if i1 != i2 {
68+
requests.push((i1, i2, trip_count));
69+
}
70+
}
71+
}
72+
}
73+
requests
74+
}
75+
76+
pub fn to_gj(&self, map: &MapModel) -> GeoJson {
77+
// Per (from, to) pair, how many trips?
78+
let mut from: Vec<Vec<usize>> =
79+
std::iter::repeat_with(|| std::iter::repeat(0).take(self.zones.len()).collect())
80+
.take(self.zones.len())
81+
.collect();
82+
// Per (to, from) pair, how many trips?
83+
let mut to: Vec<Vec<usize>> =
84+
std::iter::repeat_with(|| std::iter::repeat(0).take(self.zones.len()).collect())
85+
.take(self.zones.len())
86+
.collect();
87+
88+
for (zone1, zone2, count) in &self.desire_lines {
89+
from[zone1.0][zone2.0] += count;
90+
to[zone2.0][zone1.0] += count;
91+
}
92+
93+
let mut features = Vec::new();
94+
for (idx, zone) in self.zones.iter().enumerate() {
95+
let mut f = map.mercator.to_wgs84_gj(&zone.geometry);
96+
f.set_property("name", zone.name.clone());
97+
f.set_property("counts_from", std::mem::take(&mut from[idx]));
98+
f.set_property("counts_to", std::mem::take(&mut to[idx]));
99+
features.push(f);
100+
}
101+
GeoJson::from(features)
102+
}
103+
}
104+
105+
#[derive(Serialize, Deserialize)]
106+
pub struct Zone {
107+
/// An original opaque string ID, from different data sources.
108+
pub name: String,
109+
/// WGS84 when built originally and serialized, then Mercator right before being used
110+
pub geometry: MultiPolygon,
111+
/// The bbox, rounded to centimeters, for generate_range to work. Only calculated right before
112+
/// use.
113+
#[serde(skip_deserializing, skip_serializing)]
114+
pub x1: i64,
115+
#[serde(skip_deserializing, skip_serializing)]
116+
pub y1: i64,
117+
#[serde(skip_deserializing, skip_serializing)]
118+
pub x2: i64,
119+
#[serde(skip_deserializing, skip_serializing)]
120+
pub y2: i64,
121+
}
122+
123+
impl Zone {
124+
fn random_point(&self, rng: &mut WyRand) -> Point {
125+
loop {
126+
let x = (rng.generate_range(self.x1..=self.x2) as f64) / 100.0;
127+
let y = (rng.generate_range(self.y1..=self.y2) as f64) / 100.0;
128+
let pt = Point::new(x, y);
129+
if self.geometry.contains(&pt) {
130+
return pt;
131+
}
132+
}
133+
}
134+
}
135+
136+
/// Deterministically produce a bunch of OD pairs, just as a fallback when there's no real data
137+
pub fn synthetic_od_requests(map: &MapModel) -> Vec<(IntersectionID, IntersectionID, usize)> {
138+
let num_requests = 1_000;
139+
140+
let mut rng = WyRand::new_seed(42);
141+
let mut requests = Vec::new();
142+
for _ in 0..num_requests {
143+
let i1 = IntersectionID(rng.generate_range(0..map.intersections.len()));
144+
let i2 = IntersectionID(rng.generate_range(0..map.intersections.len()));
145+
requests.push((i1, i2, 1));
146+
}
147+
requests
148+
}

backend/src/route.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,13 @@ impl Router {
130130
pub fn od_to_counts(
131131
&self,
132132
map: &MapModel,
133-
requests: &Vec<(IntersectionID, IntersectionID)>,
133+
requests: &Vec<(IntersectionID, IntersectionID, usize)>,
134134
) -> HashMap<RoadID, usize> {
135135
let mut results = HashMap::new();
136-
for (i1, i2) in requests {
136+
for (i1, i2, count) in requests {
137137
if let Some(route) = self.route_from_intersections(map, *i1, *i2) {
138138
for (r, _) in route.steps {
139-
*results.entry(r).or_insert(0) += 1;
139+
*results.entry(r).or_insert(0) += *count;
140140
}
141141
}
142142
}

0 commit comments

Comments
 (0)