diff --git a/od2net/src/lib.rs b/od2net/src/lib.rs index 6e86b17..92c1b23 100644 --- a/od2net/src/lib.rs +++ b/od2net/src/lib.rs @@ -29,7 +29,8 @@ pub struct OutputMetadata { pub num_destinations: usize, pub num_requests: usize, pub num_succeeded_requests: usize, - pub num_failed_requests: usize, + pub num_failed_requests_same_endpoints: usize, + pub num_failed_requests_no_path: usize, pub num_edges_with_count: usize, pub routing_time_seconds: f32, pub total_meters_not_allowed: f64, @@ -55,8 +56,9 @@ impl OutputMetadata { num_origins: counts.count_per_origin.len(), num_destinations: counts.count_per_destination.len(), num_requests, - num_succeeded_requests: num_requests - (counts.errors as usize), - num_failed_requests: counts.errors as usize, + num_succeeded_requests: num_requests - counts.num_errors(), + num_failed_requests_same_endpoints: counts.errors_same_endpoints.len(), + num_failed_requests_no_path: counts.errors_no_path.len(), num_edges_with_count: counts.count_per_edge.len(), routing_time_seconds: routing_time.as_secs_f32(), total_time_seconds: None, @@ -76,7 +78,14 @@ impl OutputMetadata { ("Destinations", self.num_destinations), ("Requests", self.num_requests), ("Requests (succeeded)", self.num_succeeded_requests), - ("Requests (failed)", self.num_failed_requests), + ( + "Requests (failed because same endpoints)", + self.num_failed_requests_same_endpoints, + ), + ( + "Requests (failed because no path)", + self.num_failed_requests_no_path, + ), ("Edges with a count", self.num_edges_with_count), ] { println!("- {label}: {}", HumanCount(count as u64)); diff --git a/od2net/src/main.rs b/od2net/src/main.rs index bb09a85..c1515ca 100644 --- a/od2net/src/main.rs +++ b/od2net/src/main.rs @@ -19,6 +19,9 @@ struct Args { /// Don't output a CSV file with each edge's counts. #[clap(long)] no_output_csv: bool, + /// Don't output a GeoJSON file with failed requests. + #[clap(long)] + no_output_failed_requests: bool, /// Don't output origin and destination points in the GeoJSON output, to reduce file size. #[clap(long)] no_output_od_points: bool, @@ -157,8 +160,8 @@ fn main() -> Result<()> { ); println!( "{} succeeded, and {} failed", - HumanCount(num_requests as u64 - counts.errors), - HumanCount(counts.errors), + HumanCount(num_requests as u64 - counts.num_errors() as u64), + HumanCount(counts.num_errors() as u64), ); let routing_time = Instant::now().duration_since(routing_start); timer.stop(); @@ -169,6 +172,15 @@ fn main() -> Result<()> { timer.stop(); } + if !args.no_output_failed_requests { + timer.start("Writing failed requests GJ"); + write_failed_requests( + format!("{directory}/output/failed_requests.geojson"), + &counts, + )?; + timer.stop(); + } + let mut output_metadata = od2net::OutputMetadata::new(config, &counts, num_requests, routing_time); timer.start("Writing output GJ"); @@ -224,3 +236,19 @@ fn main() -> Result<()> { Ok(()) } + +fn write_failed_requests(path: String, counts: &od2net::network::Counts) -> Result<()> { + let mut writer = + geojson::FeatureWriter::from_writer(std::io::BufWriter::new(fs_err::File::create(path)?)); + for req in &counts.errors_same_endpoints { + let mut f = req.as_feature(); + f.set_property("reason", "same endpoints"); + writer.write_feature(&f)?; + } + for req in &counts.errors_no_path { + let mut f = req.as_feature(); + f.set_property("reason", "no path"); + writer.write_feature(&f)?; + } + Ok(writer.finish()?) +} diff --git a/od2net/src/network/mod.rs b/od2net/src/network/mod.rs index 341cd8c..aeb50d7 100644 --- a/od2net/src/network/mod.rs +++ b/od2net/src/network/mod.rs @@ -15,6 +15,8 @@ use serde::{Deserialize, Serialize}; use lts::{Tags, LTS}; +use super::requests::Request; + #[derive(Serialize, Deserialize)] pub struct Network { // TODO Doesn't handle multiple edges between the same node pair @@ -27,7 +29,11 @@ pub struct Network { pub struct Counts { // TODO Don't use f64 -- we'll end up rounding somewhere anyway, so pick a precision upfront. pub count_per_edge: HashMap<(NodeID, NodeID), f64>, - pub errors: u64, + + /// These requests failed because the start and end snapped to the same intersection + pub errors_same_endpoints: Vec, + /// These requests failed because there's no path + pub errors_no_path: Vec, // Count how many times a point is used successfully as an origin or destination pub count_per_origin: HashMap, @@ -41,7 +47,8 @@ impl Counts { pub fn new() -> Self { Self { count_per_edge: HashMap::new(), - errors: 0, + errors_same_endpoints: Vec::new(), + errors_no_path: Vec::new(), count_per_origin: HashMap::new(), count_per_destination: HashMap::new(), @@ -52,7 +59,10 @@ impl Counts { /// Adds other to this one pub fn combine(&mut self, other: Counts) { - self.errors += other.errors; + self.errors_same_endpoints + .extend(other.errors_same_endpoints); + self.errors_no_path.extend(other.errors_no_path); + for (key, count) in other.count_per_edge { *self.count_per_edge.entry(key).or_insert(0.0) += count; } @@ -66,6 +76,10 @@ impl Counts { self.total_distance_by_lts[i] += other.total_distance_by_lts[i]; } } + + pub fn num_errors(&self) -> usize { + self.errors_same_endpoints.len() + self.errors_no_path.len() + } } impl Network { diff --git a/od2net/src/requests.rs b/od2net/src/requests.rs index 93e13f9..5c4a16f 100644 --- a/od2net/src/requests.rs +++ b/od2net/src/requests.rs @@ -1,7 +1,7 @@ use anyhow::Result; use fs_err::File; -use geojson::{FeatureReader, Geometry, Value}; +use geojson::{Feature, FeatureReader, Geometry, Value}; #[derive(Debug)] pub struct Request { @@ -20,6 +20,13 @@ impl Request { serde_json::to_string(&geometry).unwrap() } + pub fn as_feature(&self) -> Feature { + Feature::from(Geometry::new(Value::LineString(vec![ + vec![self.x1, self.y1], + vec![self.x2, self.y2], + ]))) + } + pub fn load_from_geojson(path: String) -> Result> { let reader = FeatureReader::from_reader(std::io::BufReader::new(File::open(path)?)); let mut requests = Vec::new(); diff --git a/od2net/src/router.rs b/od2net/src/router.rs index cf69bc7..f80eba0 100644 --- a/od2net/src/router.rs +++ b/od2net/src/router.rs @@ -96,7 +96,7 @@ pub fn handle_request( .unwrap() .data; if start == end { - counts.errors += 1; + counts.errors_same_endpoints.push(req); return; } @@ -112,48 +112,48 @@ pub fn handle_request( ); } - if let Some(path) = path_calc.calc_path(&prepared_ch.ch, start, end) { - // fast_paths returns the total cost, but it's not necessarily the right unit. - // Calculate how long this route is. - let mut total_distance = 0.0; - for pair in path.get_nodes().windows(2) { - let i1 = prepared_ch.node_map.translate_id(pair[0]); - let i2 = prepared_ch.node_map.translate_id(pair[1]); - let edge = network - .edges - .get(&(i1, i2)) - .or_else(|| network.edges.get(&(i2, i1))) - .unwrap(); - total_distance += edge.length_meters; - - counts.total_distance_by_lts[edge.lts as u8 as usize] += edge.length_meters; - } - - let count = uptake::calculate_uptake(uptake, total_distance); - // TODO Pick an epsilon based on the final rounding we do... though it's possible 1e6 trips - // cross a segment each with probability 1e-6? - if count == 0.0 { - return; - } + let Some(path) = path_calc.calc_path(&prepared_ch.ch, start, end) else { + counts.errors_no_path.push(req); + return; + }; + // fast_paths returns the total cost, but it's not necessarily the right unit. Calculate how + // long this route is. + let mut total_distance = 0.0; + for pair in path.get_nodes().windows(2) { + let i1 = prepared_ch.node_map.translate_id(pair[0]); + let i2 = prepared_ch.node_map.translate_id(pair[1]); + let edge = network + .edges + .get(&(i1, i2)) + .or_else(|| network.edges.get(&(i2, i1))) + .unwrap(); + total_distance += edge.length_meters; + + counts.total_distance_by_lts[edge.lts as u8 as usize] += edge.length_meters; + } - for pair in path.get_nodes().windows(2) { - // TODO Actually, don't do this translation until the very end - let i1 = prepared_ch.node_map.translate_id(pair[0]); - let i2 = prepared_ch.node_map.translate_id(pair[1]); - *counts.count_per_edge.entry((i1, i2)).or_insert(0.0) += count; - } + let count = uptake::calculate_uptake(uptake, total_distance); + // TODO Pick an epsilon based on the final rounding we do... though it's possible 1e6 trips + // cross a segment each with probability 1e-6? + if count == 0.0 { + return; + } - *counts - .count_per_origin - .entry(Position::from_degrees(req.x1, req.y1)) - .or_insert(0.0) += count; - *counts - .count_per_destination - .entry(Position::from_degrees(req.x2, req.y2)) - .or_insert(0.0) += count; - } else { - counts.errors += 1; + for pair in path.get_nodes().windows(2) { + // TODO Actually, don't do this translation until the very end + let i1 = prepared_ch.node_map.translate_id(pair[0]); + let i2 = prepared_ch.node_map.translate_id(pair[1]); + *counts.count_per_edge.entry((i1, i2)).or_insert(0.0) += count; } + + *counts + .count_per_origin + .entry(Position::from_degrees(req.x1, req.y1)) + .or_insert(0.0) += count; + *counts + .count_per_destination + .entry(Position::from_degrees(req.x2, req.y2)) + .or_insert(0.0) += count; } #[derive(Serialize, Deserialize)]