-
Notifications
You must be signed in to change notification settings - Fork 2
WCI refactor #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
WCI refactor #157
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
24563ef
Beginning of WCI refactor. Point of entry for all OSM network modific…
admrtin d470f55
Merge branch 'main' into amm/wci-refactor
admrtin 8e553d1
cargo fmt
admrtin 8c1f510
wci score computation (sidewalk + cycle + speed limit + signal) score…
admrtin d7e25e1
distance value for .locate_within_distance query is now a constant
admrtin 39ab2ce
Return -6 for non-walk-eligible ways to maintain edge ordering
admrtin 3854d95
added `WCIScoreByComponent` and additional plotting mechanisms to hel…
admrtin 7228572
remove (accidentally committed) output files
admrtin b1e2324
move total score to top of structure
admrtin e191d0e
fix traffic speed score to weigh by speeds directly instead of wci sp…
admrtin 351206a
docstrings, cargo clippy, cargo fmt
admrtin c5a08b9
testing scheme
admrtin c006787
WCI score unit tests, gitignore fix
admrtin 123f12a
Merge branch 'main' into amm/wci-refactor
admrtin f5ade92
Test for neighbor WCI contribution
admrtin 61b48f2
Merge branch 'amm/wci-refactor' of https://github.com/NatLabRockies/b…
admrtin 45e5794
incorporate WciScore newtype instead of i32 and consolidate component…
admrtin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| pub mod wci; | ||
| pub mod network; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /// The CyclewayTag is a type that characterizes the | ||
| /// level of safety of a cycleway. | ||
| /// | ||
| /// You can generate a new cycleway tag by passing in | ||
| /// the OSM way's cycleway attribute. | ||
| #[derive(Debug)] | ||
| pub enum CyclewayTag { | ||
| DedicatedNoBuffer, | ||
| NoDedicatedWithFacilities, | ||
| NoDedicatedNoFacilities, | ||
| } | ||
|
|
||
| impl CyclewayTag { | ||
| pub fn new(tag: &str) -> Self { | ||
| if Self::is_dedicated_no_buffer(tag) { | ||
| CyclewayTag::DedicatedNoBuffer | ||
| } else if Self::is_no_dedicated_with_facilities(tag) { | ||
| CyclewayTag::NoDedicatedWithFacilities | ||
| } else { | ||
| CyclewayTag::NoDedicatedNoFacilities | ||
| } | ||
| } | ||
| //TODO: ADD DedicatedWithBuffer variant for LTS computations. | ||
| /// DedicatedNoBuffer variant is when a cycleway | ||
| /// is it's own dedicated space, but is still | ||
| /// a part of the road. | ||
| fn is_dedicated_no_buffer(tag: &str) -> bool { | ||
| matches!(tag, "lane" | "designated" | "track") | ||
| } | ||
|
|
||
| /// NoDedicatedWithFacilities variant is when a | ||
| /// cycleway does not have a designated lane, but | ||
| /// has some facilities for cycling awareness such as | ||
| /// signage for sharing the road with vehicle traffic. | ||
| fn is_no_dedicated_with_facilities(tag: &str) -> bool { | ||
| matches!(tag, "crossing" | "shared" | "shared_lane") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| pub mod cycleway_tag; | ||
| pub mod ops; | ||
| pub mod way_rtree_entry; | ||
| pub const MIN_DISTANCE_RTREE_NEIGHBOR: f32 = 0.0001378; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| use super::MIN_DISTANCE_RTREE_NEIGHBOR; | ||
| use crate::app::network::common::way_rtree_entry::WayRTreeEntry; | ||
| use crate::model::osm::graph::OsmNodeDataSerializable; | ||
| use crate::model::osm::graph::OsmWayDataSerializable; | ||
| use rstar::RTree; | ||
| use std::error::Error; | ||
|
|
||
| /// Find the neighboring ways in the RTree from a given way centroid | ||
| pub fn find_neighboring_ways<'a>( | ||
| query_entry: &WayRTreeEntry, | ||
| rtree: &'a RTree<WayRTreeEntry>, | ||
| ) -> Vec<&'a WayRTreeEntry> { | ||
| rtree | ||
| .locate_within_distance( | ||
| [query_entry.centroid.x(), query_entry.centroid.y()], | ||
| MIN_DISTANCE_RTREE_NEIGHBOR, | ||
| ) | ||
| .filter(|entry_in_rtree| entry_in_rtree.way.osmid != query_entry.way.osmid) | ||
| .collect() | ||
| } | ||
|
|
||
| /// Load ways from a CSV file and create R-tree entries for each way. | ||
| /// TODO: incorporate Overture's way attributes and move the logic for WayRTreeEntry up to bambam-core. | ||
| pub fn load_way_rtree_entries( | ||
| edges_file: &str, | ||
| nodes: &[OsmNodeDataSerializable], | ||
| ) -> Result<Vec<WayRTreeEntry>, Box<dyn Error>> { | ||
| let mut edge_reader = csv::Reader::from_path(edges_file)?; | ||
| let mut way_entries = Vec::new(); | ||
|
|
||
| for record in edge_reader.deserialize::<OsmWayDataSerializable>() { | ||
| let way = match record { | ||
| Ok(way) => way, | ||
| Err(err) => { | ||
| eprintln!("Error reading row: {err}"); | ||
| continue; | ||
| } | ||
| }; | ||
|
|
||
| if nodes.get(way.src_vertex_id.0).is_none() { | ||
| eprintln!( | ||
| "Warning: source vertex {} not found for way {}; skipping", | ||
| way.src_vertex_id.0, way.osmid | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| let osmid = way.osmid; | ||
| let Some(entry) = WayRTreeEntry::new(way) else { | ||
| eprintln!("Warning: could not create R-tree entry for way {osmid}"); | ||
| continue; | ||
| }; | ||
|
|
||
| way_entries.push(entry); | ||
| } | ||
|
|
||
| Ok(way_entries) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| use crate::model::osm::graph::OsmWayDataSerializable; | ||
| use geo::{BoundingRect, Centroid, Distance, Euclidean}; | ||
| use rstar::{PointDistance, RTreeObject, AABB}; | ||
|
|
||
| /// `WayRTreeEntry` wraps `OsmWayDataSerializable` and caches the bounding box | ||
| /// and centroid of the way's `linestring`. It is used solely for efficient spatial queries | ||
| /// in an R-tree data structure. | ||
| /// | ||
| /// It is used in spatial queries for network analysis, such as computing | ||
| /// the Walking Comfort Index (WCI) or Level of Traffic Stress (LTS) for a way using | ||
| /// information from the way's geometry, attributes, and nearby ways. | ||
| /// | ||
| /// If we were to implement the `RTreeObject` trait directly on `OsmWayDataSerializable`, | ||
| /// we would have to compute the bounding box every time the `envelope()` method | ||
| /// is called, which is inefficient. | ||
| /// | ||
| /// This allows us to compute the bounding box and centroid once, and reuse them in O(1) | ||
| /// for multiple spatial queries. | ||
| #[derive(Clone)] | ||
| pub struct WayRTreeEntry { | ||
| bbox: AABB<[f32; 2]>, | ||
| pub centroid: geo::Point<f32>, | ||
| pub way: OsmWayDataSerializable, | ||
| } | ||
|
|
||
| impl WayRTreeEntry { | ||
| pub fn new(way: OsmWayDataSerializable) -> Option<Self> { | ||
| // Grab the bounding rectangle of the linestring. If it doesn't exist, return None. | ||
|
admrtin marked this conversation as resolved.
|
||
| let rect = way.linestring.bounding_rect()?; | ||
|
|
||
| // Compute the centroid of the linestring. If it doesn't exist, return None. | ||
| let centroid = way.linestring.centroid()?; | ||
|
|
||
| // Create the bounding box from the linestring's bounding rectangle | ||
| Some(Self { | ||
| bbox: AABB::from_corners([rect.min().x, rect.min().y], [rect.max().x, rect.max().y]), | ||
| centroid, | ||
| way, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl RTreeObject for WayRTreeEntry { | ||
| type Envelope = AABB<[f32; 2]>; // Envelope should be the same type as the bbox of WayRTreeEntry | ||
| fn envelope(&self) -> Self::Envelope { | ||
| self.bbox // return the cached bounding box | ||
| } | ||
| } | ||
|
|
||
| impl PointDistance for WayRTreeEntry { | ||
| // NOTE: The PointDistance trait for WayRTreeEntry uses euclidean distance. | ||
| // We may want to consider using haversine distance since we are working with geographic coordinates. | ||
| // However, for small distances (in the case of local navigation), the difference may be negligible. | ||
| fn distance_2(&self, point: &[f32; 2]) -> f32 { | ||
| let query_point = geo::Point::new(point[0], point[1]); | ||
| let distance = Euclidean.distance(self.centroid, query_point); | ||
| distance * distance | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| mod common; | ||
| pub mod lts; | ||
| pub mod wci; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| use crate::app::network::wci::compute_wci::compute_wci; | ||
| use crate::app::network::wci::wci_score::WciError; | ||
| use crate::app::network::{ | ||
| common::ops::load_way_rtree_entries, wci::compute_wci::WciComponentScores, | ||
| }; | ||
| use crate::model::osm::graph::OsmNodeDataSerializable; | ||
| use kdam::{Bar, BarBuilder, BarExt}; | ||
| use rayon::prelude::*; | ||
| use routee_compass_core::util::fs::read_utils; | ||
| use rstar::RTree; | ||
| use std::sync::{Arc, Mutex}; | ||
| use std::{ | ||
| error::Error, | ||
| fs::File, | ||
| io::{BufWriter, Write}, | ||
| }; | ||
|
|
||
| /// Bulk compute WCI scores for an OSM network by taking in a vertices-complete.csv | ||
| /// and edges-complete.csv. | ||
| /// | ||
| /// Reads the verticies and edges into memory, constructs an R-tree for spatial queries, | ||
| /// computes the WCI score for each way in parallel, and writes the scores to an output file. | ||
| /// | ||
| /// WCI scores are computed in parallel and written line-by-line to `output_file`. | ||
| /// Authors: EG (2025 original), AM (2026 refactor) | ||
| pub fn bulk_compute_wci( | ||
| edges_file: &str, | ||
| vertices_file: &str, | ||
| output_file: &str, | ||
| ) -> Result<(), Box<dyn Error>> { | ||
| println!("Reading:\n\t- vertex set @ {vertices_file}\n\t- edge set @ {edges_file}"); | ||
| // load all vertices into memory | ||
| let vertices: Box<[OsmNodeDataSerializable]> = | ||
| read_utils::from_csv(&vertices_file, true, None, None)?; | ||
| // load all ways into memory as type WayRTreeEntry for insertion into the R-tree | ||
| let way_rtree_entries = load_way_rtree_entries(edges_file, &vertices)?; | ||
| println!("Edges and vertices read successfully."); | ||
| // bulk-load a copy of the entries into the r-tree; we keep `entries` around | ||
| // so each centroid can be paired with its own way during the WCI calculation | ||
| let rtree = RTree::bulk_load(way_rtree_entries.clone()); | ||
|
|
||
| let bar: Arc<Mutex<Bar>> = Arc::new(Mutex::new( | ||
| BarBuilder::default() | ||
| .desc(format!("Computing WCI scores for the road network")) | ||
| .total(way_rtree_entries.len()) | ||
| .build()?, | ||
| )); | ||
|
|
||
| // calculate the WCI component scores for each way in parallel using the compute_wci function | ||
| let wci_vec_with_components: Vec<WciComponentScores> = way_rtree_entries | ||
| .par_iter() | ||
| .map(|way_entry| -> Result<WciComponentScores, WciError> { | ||
| // compute wci for this entry | ||
| let wci = compute_wci( | ||
| &rtree, | ||
| way_entry, | ||
| vertices | ||
| .get(way_entry.way.src_vertex_id.0) | ||
| .unwrap_or(&OsmNodeDataSerializable::default()), | ||
| ); | ||
| // update pbar | ||
| if let Ok(mut bar) = bar.clone().lock() { | ||
| let _ = bar.update(1); | ||
| } | ||
| // return wci result | ||
| wci | ||
| }) | ||
| .collect::<Result<Vec<WciComponentScores>, WciError>>()?; | ||
|
|
||
| let file = File::create(output_file)?; | ||
| let mut writer = BufWriter::new(file); | ||
| // write header | ||
| writeln!(writer, "wci_total,wci_walk,wci_speed,wci_cycle,wci_signal")?; | ||
| // write values | ||
| for wci in &wci_vec_with_components { | ||
| writeln!( | ||
| writer, | ||
| "{},{},{},{},{}", | ||
| wci.total_score, | ||
| wci.walkability_score | ||
| .as_ref() | ||
| .map_or(String::new(), |v| v.to_string()), | ||
| wci.traffic_speed_score | ||
| .as_ref() | ||
| .map_or(String::new(), |v| v.to_string()), | ||
| wci.cycleway_score | ||
| .as_ref() | ||
| .map_or(String::new(), |v| v.to_string()), | ||
| wci.traffic_signal_score | ||
| .as_ref() | ||
| .map_or(String::new(), |v| v.to_string()), | ||
| )?; | ||
| } | ||
| writer.flush()?; | ||
| println!("WCI scores computed successfully.\nWCI scores file saved @ {output_file}."); | ||
| Ok(()) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.