open-standards/map/map-rs/crates/map-route/src/transit.rs
Transaction Science ba1212361f Add aac, map, sandbox standards; catalog the standards family
Commit the previously-untracked standard directories and catalog them in the
root README alongside proof:

- aac/    — communication cargo class: portable boards + a ten-level
            offline-first cascade (Apache-2.0 code / CC-BY-4.0 spec).
- map/    — geospatial cargo class: range-requestable signed tilesets +
            an F0–F4 fidelity cascade (Apache-2.0 code / CC-BY-4.0 spec).
- sandbox/— isolated, metered, attested execution across the heterogeneous
            compute fabric (Apache-2.0 code / CC-BY-4.0 spec).
- jouleclaw-otel-authz, jouleclaw-trust — new jouleclaw reference crates.

Root README now lists aac, map, sandbox, and proof in the catalog, the tree,
and the contribution guide.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-07 17:04:36 -04:00

277 lines
10 KiB
Rust

//! Public-transit routing — earliest arrival over a timetable via the
//! **Connection Scan Algorithm** (CSA).
//!
//! Where [`crate::find_route`] and [`crate::cost`] route a continuous road graph,
//! transit is *time-dependent*: you can only board a vehicle at its scheduled
//! departure. A [`Timetable`] is a list of [`Connection`]s (one scheduled hop
//! between consecutive stops) plus walking [`footpaths`] for transfers. CSA scans
//! the connections once in departure order, relaxing the earliest arrival at each
//! stop — `O(connections)` per query after a sort, and exact for earliest arrival.
//!
//! Times are integer seconds (e.g. since service midnight), matching GTFS.
//!
//! [`footpaths`]: Timetable::add_footpath
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::{haversine_distance, Coordinate};
const INF: i64 = i64::MAX;
/// One scheduled vehicle hop between two consecutive stops on a trip.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Connection {
pub trip: u64,
pub dep_stop: u64,
pub arr_stop: u64,
pub dep_time: i64,
pub arr_time: i64,
}
/// A transit network: stops, scheduled connections, and walking transfers.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Timetable {
pub stops: BTreeMap<u64, Coordinate>,
pub connections: Vec<Connection>,
/// `stop -> [(reachable stop, walk seconds)]`.
footpaths: BTreeMap<u64, Vec<(u64, i64)>>,
}
impl Timetable {
pub fn add_stop(&mut self, id: u64, coord: Coordinate) {
self.stops.insert(id, coord);
}
pub fn add_connection(&mut self, c: Connection) {
self.connections.push(c);
}
/// Add a bidirectional walking transfer between two stops.
pub fn add_footpath(&mut self, a: u64, b: u64, secs: i64) {
self.footpaths.entry(a).or_default().push((b, secs));
self.footpaths.entry(b).or_default().push((a, secs));
}
/// Generate transfers between every pair of stops within `max_dist_m`,
/// timed at `walk_speed_mps` (e.g. 1.4). Replaces existing footpaths.
pub fn auto_footpaths(&mut self, max_dist_m: f64, walk_speed_mps: f64) {
self.footpaths.clear();
let stops: Vec<(u64, Coordinate)> = self.stops.iter().map(|(k, v)| (*k, *v)).collect();
for (a, ca) in &stops {
for (b, cb) in &stops {
if a == b {
continue;
}
let d = haversine_distance(ca, cb);
if d <= max_dist_m {
let secs = (d / walk_speed_mps).ceil() as i64;
self.footpaths.entry(*a).or_default().push((*b, secs));
}
}
}
}
/// Earliest arrival from `source` to `target` departing no earlier than
/// `dep_time`. Returns the journey (transit + walk legs) or `None` if the
/// target is unreachable.
pub fn earliest_arrival(&self, source: u64, target: u64, dep_time: i64) -> Option<Journey> {
if !self.stops.contains_key(&source) || !self.stops.contains_key(&target) {
return None;
}
let mut arr: BTreeMap<u64, i64> = BTreeMap::new();
let mut pred: BTreeMap<u64, Pred> = BTreeMap::new();
arr.insert(source, dep_time);
// Initial walking transfers from the source.
self.relax_footpaths(source, dep_time, &mut arr, &mut pred);
// Scan connections in departure order.
let mut order: Vec<usize> = (0..self.connections.len()).collect();
order.sort_by_key(|&i| self.connections[i].dep_time);
for &i in &order {
let c = self.connections[i];
if c.dep_time < dep_time {
continue;
}
let reach_dep = arr.get(&c.dep_stop).copied().unwrap_or(INF);
if reach_dep <= c.dep_time && c.arr_time < arr.get(&c.arr_stop).copied().unwrap_or(INF) {
arr.insert(c.arr_stop, c.arr_time);
pred.insert(c.arr_stop, Pred::Conn(i));
self.relax_footpaths(c.arr_stop, c.arr_time, &mut arr, &mut pred);
}
}
let arrival_time = arr.get(&target).copied().unwrap_or(INF);
if arrival_time == INF {
return None;
}
self.reconstruct(source, target, arrival_time, &pred)
}
fn relax_footpaths(
&self,
from: u64,
at: i64,
arr: &mut BTreeMap<u64, i64>,
pred: &mut BTreeMap<u64, Pred>,
) {
if let Some(fps) = self.footpaths.get(&from) {
for &(to, secs) in fps {
let cand = at + secs;
if cand < arr.get(&to).copied().unwrap_or(INF) {
arr.insert(to, cand);
pred.insert(to, Pred::Walk { from, secs });
}
}
}
}
fn reconstruct(
&self,
source: u64,
target: u64,
arrival_time: i64,
pred: &BTreeMap<u64, Pred>,
) -> Option<Journey> {
let mut legs = Vec::new();
let mut cur = target;
let mut guard = 0usize;
let limit = self.connections.len() + self.stops.len() + 2;
while cur != source {
guard += 1;
if guard > limit {
return None; // defensive: malformed predecessor chain
}
match pred.get(&cur)? {
Pred::Conn(i) => {
let c = self.connections[*i];
legs.push(Leg::Transit {
trip: c.trip,
from: c.dep_stop,
to: c.arr_stop,
dep: c.dep_time,
arr: c.arr_time,
});
cur = c.dep_stop;
}
Pred::Walk { from, secs } => {
legs.push(Leg::Walk { from: *from, to: cur, secs: *secs });
cur = *from;
}
}
}
legs.reverse();
Some(Journey { arrival_time, legs })
}
}
/// How a stop's earliest arrival was achieved (for path reconstruction).
#[derive(Debug, Clone, Copy)]
enum Pred {
Conn(usize),
Walk { from: u64, secs: i64 },
}
/// One leg of a transit journey.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Leg {
Transit { trip: u64, from: u64, to: u64, dep: i64, arr: i64 },
Walk { from: u64, to: u64, secs: i64 },
}
/// A computed transit journey: when it arrives and the legs to get there.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Journey {
pub arrival_time: i64,
pub legs: Vec<Leg>,
}
impl Journey {
/// Number of vehicle boardings (transfers + 1, or 0 for a walk-only trip).
pub fn boardings(&self) -> usize {
self.legs.iter().filter(|l| matches!(l, Leg::Transit { .. })).count()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tt() -> Timetable {
let mut t = Timetable::default();
for id in 1..=4 {
t.add_stop(id, Coordinate::new(0.0, id as f64 * 0.01));
}
t
}
#[test]
fn earliest_arrival_along_a_line_with_a_wait() {
let mut t = tt();
// Trip 10: stop1 @0 → stop2 @100, then (after a wait) stop2 @120 → stop3 @200.
t.add_connection(Connection { trip: 10, dep_stop: 1, arr_stop: 2, dep_time: 0, arr_time: 100 });
t.add_connection(Connection { trip: 10, dep_stop: 2, arr_stop: 3, dep_time: 120, arr_time: 200 });
let j = t.earliest_arrival(1, 3, 0).expect("reachable");
assert_eq!(j.arrival_time, 200);
assert_eq!(j.boardings(), 2);
assert!(matches!(j.legs[0], Leg::Transit { to: 2, .. }));
assert!(matches!(j.legs[1], Leg::Transit { to: 3, .. }));
}
#[test]
fn missing_connection_in_time_is_unreachable() {
let mut t = tt();
// Only a connection that departs before our departure time.
t.add_connection(Connection { trip: 10, dep_stop: 1, arr_stop: 3, dep_time: 0, arr_time: 50 });
assert!(t.earliest_arrival(1, 3, 100).is_none()); // missed it
assert!(t.earliest_arrival(1, 3, 0).is_some());
}
#[test]
fn picks_the_faster_of_two_trips() {
let mut t = tt();
// Slow trip 1→3 arriving 300; fast trip 1→3 arriving 150.
t.add_connection(Connection { trip: 1, dep_stop: 1, arr_stop: 3, dep_time: 0, arr_time: 300 });
t.add_connection(Connection { trip: 2, dep_stop: 1, arr_stop: 3, dep_time: 10, arr_time: 150 });
let j = t.earliest_arrival(1, 3, 0).unwrap();
assert_eq!(j.arrival_time, 150);
assert!(matches!(j.legs[0], Leg::Transit { trip: 2, .. }));
}
#[test]
fn transfer_via_footpath_between_trips() {
let mut t = tt();
// Trip A reaches stop 2 @100; trip B departs stop 4 @ 130 → stop 3 @ 200.
// A 20s walk from stop 2 to stop 4 lets us catch B.
t.add_connection(Connection { trip: 1, dep_stop: 1, arr_stop: 2, dep_time: 0, arr_time: 100 });
t.add_connection(Connection { trip: 2, dep_stop: 4, arr_stop: 3, dep_time: 130, arr_time: 200 });
t.add_footpath(2, 4, 20);
let j = t.earliest_arrival(1, 3, 0).expect("reachable via transfer");
assert_eq!(j.arrival_time, 200);
// Ride, walk, ride.
assert!(matches!(j.legs[0], Leg::Transit { to: 2, .. }));
assert!(matches!(j.legs[1], Leg::Walk { from: 2, to: 4, secs: 20 }));
assert!(matches!(j.legs[2], Leg::Transit { to: 3, .. }));
assert_eq!(j.boardings(), 2);
}
#[test]
fn auto_footpaths_enable_a_transfer() {
let mut t = Timetable::default();
// Two stops ~very close, one far.
t.add_stop(1, Coordinate::new(0.0, 0.0));
t.add_stop(2, Coordinate::new(0.0, 0.00001)); // ~1 m from stop 1
t.add_stop(3, Coordinate::new(0.0, 1.0));
t.add_connection(Connection { trip: 1, dep_stop: 2, arr_stop: 3, dep_time: 60, arr_time: 600 });
t.auto_footpaths(50.0, 1.4); // stop1↔stop2 only
let j = t.earliest_arrival(1, 3, 0).expect("walk to stop 2, then ride");
assert_eq!(j.arrival_time, 600);
assert!(matches!(j.legs[0], Leg::Walk { from: 1, to: 2, .. }));
}
}