Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"bambam-gbfs",
"bambam-gtfs",
"bambam-gtfs-flex",
"bambam-modal-metrics",
"bambam-omf",
"bambam-osm",
"bambam-py",
Expand All @@ -21,7 +22,8 @@ bambam = { version = "0.3.3", path = "bambam" }
bambam-core = { version = "0.3.3", path = "bambam-core" }
bambam-gbfs = { version = "0.3.3", path = "bambam-gbfs" }
bambam-gtfs = { version = "0.3.3", path = "bambam-gtfs" }
bambam-gtfs-flex = { version = "0.3.3", path = "bambam-gtfs-flex" }
bambam-gtfs-flex = { version = "0.3.3", path = "bambam-gtfs-flex" }
bambam-modal-metrics = { version = "0.3.3", path = "bambam-modal-metrics" }
bambam-omf = { version = "0.3.3", path = "bambam-omf" }
bambam-osm = { version = "0.3.3", path = "bambam-osm" }
bamcensus = { version = "0.1.0" }
Expand Down
23 changes: 23 additions & 0 deletions rust/bambam-modal-metrics/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "bambam-modal-metrics"
edition = "2021"
license = "BSD-3-Clause"
version.workspace = true
readme = "README.md"
repository = "https://github.com/NatLabRockies/bambam"
documentation = "https://docs.rs/bambam-modal-metrics"
description = "Modal Metrics for road network data in The Behavior and Advanced Mobility Big Access Model"
keywords = ["nlr", "access-model", "accessibility", "multimodal", "transit"]
categories = ["science", "science::geo"]

[dependencies]
csv = { workspace = true }
geo = { workspace = true }
kdam = { workspace = true }
log = { workspace = true }
rayon = { workspace = true }
routee-compass-core = { workspace = true }
rstar = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
uom = { workspace = true }
30 changes: 30 additions & 0 deletions rust/bambam-modal-metrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# `bambam-modal-metrics`
Comment thread
admrtin marked this conversation as resolved.

## A library crate containing:

Extensible traits for road network data that can facilitate modal metric computations

The logic for computing modal metrics such as the Walking Comfort Index (WCI) or the Level of Traffic Stress (LTS)

This library is wired into `bambam` through the set of commands defined in `bamam_util`. You can run the `bambam_util modal_metric` command from the `bambam` crate to compute modal metrics such as WCI and LTS for a given road network. As of now, `bambam-modal-metrics` only supports OpenStreetMaps way/node data as input, but extension to OvertureMaps is planned.

## What is a modal metric?

A modal metric is a value that qualitatively describes links (or edges) in a transportation network for a specific modality. Two examples of this type of metric are:

### 1. **Walking Comfort Index**:
How comfortable are links in a network in terms of walkability?

#### Considerations:
- Link and neighboring link traffic speed
- Link type and characteristics (sidewalk? footway? cycleway?)
- Does the link contain either a stop sign or a traffic signal to allow for crossings and speed limiting?

### 2. **Level of Traffic Stress**:
How stressful are links in a network for cyclists?

#### Considerations:
- Link and neighboring link traffic speed
- Link type
- Cycleway infrastructure
- Single lane vs. multi-lane
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use crate::app::network::common::modal_metric::{ModalMetric, ModalMetricError, ModalMetricValue};
use crate::app::network::common::ops::load_way_rtree_entries;
use crate::model::osm::graph::OsmNodeDataSerializable;
use crate::common::modal_metrics::{ModalMetric, ModalMetricError, ModalMetricValue};
use crate::common::ops::load_edge_rtree_entries;
use crate::network_traits::{
edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge,
vertex_for_modal_metric::VertexForModalMetric,
};
use kdam::{Bar, BarBuilder, BarExt};
use rayon::prelude::*;
use routee_compass_core::util::fs::read_utils;
use rstar::RTree;
use serde::de::DeserializeOwned;
use std::sync::{Arc, Mutex};
use std::{
error::Error,
Expand All @@ -18,12 +22,17 @@ use std::{
/// `metric_name` can either be:
/// - "WCI" for the Walking Comfort Index metric
/// - "LTS" for the Level of Traffic Stress (cycling comfort) metric
pub fn bulk_compute_modal_metric(
pub fn bulk_compute_modal_metric<E, V>(
metric_name: &str,
edges_file: &str,
vertices_file: &str,
output_file: &str,
) -> Result<(), Box<dyn Error>> {
) -> Result<(), Box<dyn Error>>
where
E: SpatialEdge + EdgeForModalMetric + DeserializeOwned + Clone + Send + Sync,
V: VertexForModalMetric + DeserializeOwned + Send + Sync,
{
// determine the modal metric to compute based on the provided metric name.
let metric: ModalMetric = metric_name.parse()?;

log::info!(
Expand All @@ -32,29 +41,31 @@ pub fn bulk_compute_modal_metric(
);
log::info!("Reading:\n\t- vertex set @ {vertices_file}\n\t- edge set @ {edges_file}\n");

let vertices: Box<[OsmNodeDataSerializable]> =
read_utils::from_csv(&vertices_file, true, None, None)?;
let way_rtree_entries = load_way_rtree_entries(edges_file, &vertices)?;
// load vertices and edges.
let vertices: Box<[V]> = read_utils::from_csv(&vertices_file, true, None, None)?;
let edge_rtree_entries = load_edge_rtree_entries::<E, V>(edges_file, &vertices)?;
log::info!("Edges and vertices read successfully.\n");

let rtree = RTree::bulk_load(way_rtree_entries.clone());
// build an RTree with the edge entries.
let rtree = RTree::bulk_load(edge_rtree_entries.clone());

let bar: Arc<Mutex<Bar>> = Arc::new(Mutex::new(
BarBuilder::default()
.desc(format!(
"Computing {:?} for ways in the road network",
"Computing {:?} for edges in the road network",
metric
))
.total(way_rtree_entries.len())
.total(edge_rtree_entries.len())
.build()?,
));

let values: Vec<ModalMetricValue> = way_rtree_entries
// compute the modal metric for each edge in parallel via par_iter
let values: Vec<ModalMetricValue> = edge_rtree_entries
.par_iter()
.map(|way_entry| {
let src_node = vertices.get(way_entry.way.src_vertex_id.0);
.map(|edge_entry| {
let src_vertex = vertices.get(edge_entry.edge.src_vertex_id());

let result = metric.compute_metric(&rtree, way_entry, src_node)?;
let result = metric.compute_metric(&rtree, edge_entry, src_vertex)?;

if let Ok(mut bar) = bar.lock() {
let _ = bar.update(1);
Expand All @@ -66,6 +77,7 @@ pub fn bulk_compute_modal_metric(

eprintln!();

// write to file
let file = File::create(output_file)?;
let mut writer = BufWriter::new(file);

Expand All @@ -76,7 +88,7 @@ pub fn bulk_compute_modal_metric(
writer.flush()?;

log::info!(
"\n\n{:?} values computed successfully.\n\nOutputfile saved @ {output_file}.",
"\n\n{:?} values computed successfully.\n\nOutput file saved @ {output_file}.",
metric
);
Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
/// level of safety of a cycleway.
///
/// You can generate a new cycleway tag by passing in
/// the OSM way's cycleway attribute.
#[derive(Debug)]
/// the edge's cycleway attribute.
#[derive(Debug, Clone)]
pub enum CyclewayTag {
DedicatedWithBuffer,
DedicatedNoBuffer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,61 @@
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
use crate::network_traits::spatial_edge::SpatialEdge;

/// The maximum distance (in degrees) within which another edge is considered a
/// spatial "neighbor" for the purpose of neighbor-aware modal penalty scores.
/// Roughly 15 meters at mid latitudes.
pub const MIN_DISTANCE_RTREE_NEIGHBOR: f32 = 0.0001378;

/// `EdgeRTreeEntry` wraps the network edge and caches the bounding box
/// and centroid of the edge'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`,
/// If we were to implement the `RTreeObject` trait directly on the network edge,
/// 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 {
pub struct EdgeRTreeEntry<E> {
bbox: AABB<[f32; 2]>,
pub centroid: geo::Point<f32>,
pub way: OsmWayDataSerializable,
pub edge: E,
}

impl WayRTreeEntry {
pub fn new(way: OsmWayDataSerializable) -> Option<Self> {
impl<E: SpatialEdge> EdgeRTreeEntry<E> {
pub fn new(edge: E) -> Option<Self> {
let linestring = edge.linestring()?;
// Grab the bounding rectangle of the linestring. If it doesn't exist, return None.
let rect = way.linestring.bounding_rect()?;
let rect = linestring.bounding_rect()?;

// Compute the centroid of the linestring. If it doesn't exist, return None.
let centroid = way.linestring.centroid()?;
let centroid = 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,
edge,
})
}
}

impl RTreeObject for WayRTreeEntry {
type Envelope = AABB<[f32; 2]>; // Envelope should be the same type as the bbox of WayRTreeEntry
impl<E: SpatialEdge> RTreeObject for EdgeRTreeEntry<E> {
type Envelope = AABB<[f32; 2]>; // Envelope should be the same type as the bbox of EdgeRTreeEntry
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.
impl<E: SpatialEdge> PointDistance for EdgeRTreeEntry<E> {
// NOTE: The PointDistance trait for EdgeRTreeEntry 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 {
Expand All @@ -57,3 +64,19 @@ impl PointDistance for WayRTreeEntry {
distance * distance
}
}

/// Find the neighboring edges within [`MIN_DISTANCE_RTREE_NEIGHBOR`] of the
/// query edge's centroid, excluding the query edge itself (by [`SpatialEdge::id`]).
pub fn find_neighboring_edges<'a, E: SpatialEdge>(
query: &EdgeRTreeEntry<E>,
rtree: &'a rstar::RTree<EdgeRTreeEntry<E>>,
) -> Vec<&'a EdgeRTreeEntry<E>> {
let query_id = query.edge.id();
rtree
.locate_within_distance(
[query.centroid.x(), query.centroid.y()],
MIN_DISTANCE_RTREE_NEIGHBOR,
)
.filter(|entry| entry.edge.id() != query_id)
.collect()
}
5 changes: 5 additions & 0 deletions rust/bambam-modal-metrics/src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod bulk_compute_modal_metric;
pub mod cycleway_tag;
pub mod edge_rtree_entry;
pub mod modal_metrics;
pub mod ops;
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::app::network::common::way_rtree_entry::WayRTreeEntry;
use crate::app::network::lts::compute_lts::compute_lts;
use crate::app::network::lts::lts::Lts;
use crate::app::network::lts::lts::LtsError;
use crate::app::network::wci::compute_wci::{compute_wci, WciComponents};
use crate::app::network::wci::wci::WciError;
use crate::model::osm::graph::OsmNodeDataSerializable;
use crate::common::edge_rtree_entry::EdgeRTreeEntry;
use crate::lts::compute_lts::compute_lts;
use crate::lts::lts::Lts;
use crate::lts::lts::LtsError;
use crate::network_traits::{
edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge,
vertex_for_modal_metric::VertexForModalMetric,
};
use crate::wci::compute_wci::{compute_wci, WciComponents};
use crate::wci::wci::WciError;
use rstar::RTree;
use std::{error::Error, io::Write, str::FromStr};

Expand Down Expand Up @@ -49,25 +52,29 @@ impl FromStr for ModalMetric {
}

impl ModalMetric {
/// Computes the specified modal metric for the given way entry and source node.
pub fn compute_metric(
/// Computes the specified modal metric for the given edge entry and source vertex.
pub fn compute_metric<E, V>(
&self,
rtree: &RTree<WayRTreeEntry>,
way_entry: &WayRTreeEntry,
src_node: Option<&OsmNodeDataSerializable>,
) -> Result<ModalMetricValue, ModalMetricError> {
rtree: &RTree<EdgeRTreeEntry<E>>,
edge_entry: &EdgeRTreeEntry<E>,
src_vertex: Option<&V>,
) -> Result<ModalMetricValue, ModalMetricError>
where
E: SpatialEdge + EdgeForModalMetric,
V: VertexForModalMetric,
{
match self {
ModalMetric::WalkingComfortIndex => {
let wci = compute_wci(rtree, way_entry, src_node)?;
let wci = compute_wci(rtree, edge_entry, src_vertex)?;
Ok(ModalMetricValue::Wci(wci))
}
ModalMetric::LevelOfTrafficStress => {
let lts = compute_lts(rtree, way_entry)?;
let lts = compute_lts(rtree, edge_entry)?;
Ok(ModalMetricValue::Lts(lts))
}
}
}

/// Writes the CSV header for the specified modal metric.
pub fn write_csv_header(&self, writer: &mut impl Write) -> Result<(), Box<dyn Error>> {
match self {
ModalMetric::WalkingComfortIndex => {
Expand Down
Loading
Loading