Skip to main content

rustsim_traffic/
types.rs

1//! Transport-domain semantics layered on top of `rustsim-core` and `rustsim-spaces`.
2//!
3//! This module provides reusable transport metadata plus transport operations
4//! over `LinkSpace<LinkProperties>`.
5//!
6//! Current semantic position:
7//! - topology and occupancy primitives come from `LinkSpace<P>`
8//! - this module adds transport-specific calculations and convenience presets
9//! - queue and control decisions are explicit policies in [`crate::policy`]
10//!
11//! In particular:
12//! - `link_density`, `link_speed`, and travel-time helpers are per-link snapshot calculations
13//! - `agent_speed` uses the default FIFO/gap policy for current ordering and downstream blocking
14//! - no lane-changing, merge-resolution, signal-phase engine, or network-equilibrium model is implied
15
16use crate::policy::{FifoGapPolicy, QueuePolicy, SpeedDecision};
17use rustsim_core::types::{EdgeId, LevelRelation, NodeId, SemanticEntity, ZoneId};
18use rustsim_modes::AllowedModes;
19use rustsim_spaces::link::{LinkGeometry, LinkGeometryError, LinkId, LinkSpace, LinkSpaceError};
20
21/// Coarse traffic control semantics at a node, movement, or approach.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub enum TrafficControlType {
24    #[default]
25    Uncontrolled,
26    Yield,
27    Stop,
28    Signal,
29}
30
31/// Coarse turning movement semantics.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum TurnType {
34    Left,
35    Through,
36    Right,
37    UTurn,
38}
39
40/// Coarse classification for transport links.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
42pub enum LinkClass {
43    #[default]
44    Road,
45    Walkway,
46    Cycleway,
47    Transitway,
48    Rail,
49    Shared,
50}
51
52/// Speed-density relationship (fundamental diagram) for a transport link.
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum FundamentalDiagram {
55    Greenshields,
56    Underwood {
57        k_opt: Option<f64>,
58    },
59    Triangular,
60    FreeFlow,
61    /// Weidmann (1993) pedestrian speed-density relationship.
62    ///
63    /// Uses density in ped/m² (not veh/km/lane). The `jam_density` parameter
64    /// passed to [`speed()`](FundamentalDiagram::speed) is ignored; the
65    /// Weidmann jam density of 5.4 ped/m² is used instead.
66    ///
67    /// **Important:** when using this variant, `density_per_km` is
68    /// reinterpreted as density in ped/m².
69    Weidmann,
70}
71
72impl FundamentalDiagram {
73    /// Compute speed (m/s) given density (veh/km/lane), free-flow speed (m/s), and jam density.
74    pub fn speed(&self, density_per_km: f64, free_flow_speed: f64, jam_density: f64) -> f64 {
75        if density_per_km <= 0.0 || jam_density <= 0.0 {
76            return free_flow_speed;
77        }
78
79        let speed = match self {
80            FundamentalDiagram::Greenshields => {
81                free_flow_speed * (1.0 - density_per_km / jam_density).max(0.0)
82            }
83            FundamentalDiagram::Underwood { k_opt } => {
84                let k_opt_val = k_opt.unwrap_or(jam_density / std::f64::consts::E);
85                if k_opt_val <= 0.0 {
86                    free_flow_speed
87                } else {
88                    free_flow_speed * (-density_per_km / k_opt_val).exp()
89                }
90            }
91            FundamentalDiagram::Triangular => {
92                let k_c = jam_density / 2.0;
93                if density_per_km <= k_c {
94                    free_flow_speed
95                } else {
96                    free_flow_speed * (jam_density - density_per_km) / (jam_density - k_c)
97                }
98                .max(0.0)
99            }
100            FundamentalDiagram::FreeFlow => free_flow_speed,
101            FundamentalDiagram::Weidmann => {
102                crate::pedestrian_links::weidmann_speed(free_flow_speed, density_per_km)
103            }
104        };
105
106        speed.max(0.0)
107    }
108}
109
110/// Transport-specific physical properties of a link.
111///
112/// This type stores the transport behavior parameters attached to a
113/// `LinkSpace<LinkProperties>` link.
114///
115/// The associated constructors such as [`LinkProperties::urban`],
116/// [`LinkProperties::freeway`], and [`LinkProperties::pedestrian`] are
117/// **convenience presets** for common real-world link types. They are not
118/// a fixed catalog enforced by the engine - users can either use these
119/// helpers as starting points or construct/customize values directly.
120#[derive(Debug, Clone, Copy, PartialEq)]
121pub struct LinkProperties {
122    /// Free-flow speed in m/s.
123    pub free_flow_speed: f64,
124    /// Capacity in units/hour across all lanes.
125    pub capacity: f64,
126    /// Number of lanes or effective parallel channels.
127    pub lanes: u32,
128    /// Jam density in units/km/lane.
129    pub jam_density: f64,
130    /// Speed-density relationship applied to the link.
131    pub diagram: FundamentalDiagram,
132}
133
134impl LinkProperties {
135    /// Convenience preset for a typical urban road segment.
136    ///
137    /// Returns `(LinkGeometry, LinkProperties)` so the result can be passed
138    /// directly into `LinkSpace<LinkProperties>::add_link(...)`.
139    ///
140    /// Arguments:
141    /// - `length`: link length in meters
142    /// - `speed_kmh`: free-flow speed in km/h
143    /// - `lanes`: number of lanes
144    ///
145    /// This is an example/default profile, not a mandatory schema.
146    pub fn urban(
147        length: f64,
148        speed_kmh: f64,
149        lanes: u32,
150    ) -> Result<(LinkGeometry, Self), LinkGeometryError> {
151        Ok((
152            LinkGeometry::new(length)?,
153            Self {
154                free_flow_speed: speed_kmh / 3.6,
155                capacity: 900.0 * lanes as f64,
156                lanes,
157                jam_density: 150.0,
158                diagram: FundamentalDiagram::Greenshields,
159            },
160        ))
161    }
162
163    /// Convenience preset for a typical freeway segment.
164    ///
165    /// Returns `(LinkGeometry, LinkProperties)` so the result can be passed
166    /// directly into `LinkSpace<LinkProperties>::add_link(...)`.
167    ///
168    /// Arguments:
169    /// - `length`: link length in meters
170    /// - `speed_kmh`: free-flow speed in km/h
171    /// - `lanes`: number of lanes
172    ///
173    /// This is an example/default profile, not a mandatory schema.
174    pub fn freeway(
175        length: f64,
176        speed_kmh: f64,
177        lanes: u32,
178    ) -> Result<(LinkGeometry, Self), LinkGeometryError> {
179        Ok((
180            LinkGeometry::new(length)?,
181            Self {
182                free_flow_speed: speed_kmh / 3.6,
183                capacity: 2200.0 * lanes as f64,
184                lanes,
185                jam_density: 120.0,
186                diagram: FundamentalDiagram::Underwood { k_opt: None },
187            },
188        ))
189    }
190
191    /// Convenience preset for a pedestrian corridor or walkway.
192    ///
193    /// Returns `(LinkGeometry, LinkProperties)` so the result can be passed
194    /// directly into `LinkSpace<LinkProperties>::add_link(...)`.
195    ///
196    /// Arguments:
197    /// - `length`: link length in meters
198    /// - `width`: effective corridor width in meters
199    ///
200    /// Width is converted into an approximate number of parallel channels.
201    /// This is an example/default profile, not a mandatory schema.
202    pub fn pedestrian(length: f64, width: f64) -> Result<(LinkGeometry, Self), LinkGeometryError> {
203        Ok((
204            LinkGeometry::new(length)?,
205            Self {
206                free_flow_speed: 1.3,
207                capacity: 4800.0 * width,
208                lanes: (width.ceil() as u32).max(1),
209                jam_density: 5000.0,
210                diagram: FundamentalDiagram::Greenshields,
211            },
212        ))
213    }
214}
215
216/// Minimal reusable transport metadata for a link or edge.
217#[derive(Debug, Clone, Copy, PartialEq)]
218pub struct TransportLinkMetadata {
219    /// Stable edge identifier.
220    pub edge_id: EdgeId,
221    /// Source topology node.
222    pub from_node: NodeId,
223    /// Destination topology node.
224    pub to_node: NodeId,
225    /// Coarse link classification.
226    pub link_class: LinkClass,
227    /// Which modes may use this link.
228    pub allowed_modes: AllowedModes,
229    /// Optional traffic control semantics.
230    pub traffic_control: Option<TrafficControlType>,
231    /// Optional turn semantics.
232    pub turn_type: Option<TurnType>,
233    /// Optional speed limit in km/h.
234    pub speed_limit_kph: Option<f64>,
235}
236
237impl TransportLinkMetadata {
238    /// Create link metadata with a required topology and mode definition.
239    pub fn new(
240        edge_id: EdgeId,
241        from_node: NodeId,
242        to_node: NodeId,
243        link_class: LinkClass,
244        allowed_modes: AllowedModes,
245    ) -> Self {
246        Self {
247            edge_id,
248            from_node,
249            to_node,
250            link_class,
251            allowed_modes,
252            traffic_control: None,
253            turn_type: None,
254            speed_limit_kph: None,
255        }
256    }
257
258    /// Attach traffic control semantics.
259    pub fn with_traffic_control(mut self, traffic_control: TrafficControlType) -> Self {
260        self.traffic_control = Some(traffic_control);
261        self
262    }
263
264    /// Attach turning movement semantics.
265    pub fn with_turn_type(mut self, turn_type: TurnType) -> Self {
266        self.turn_type = Some(turn_type);
267        self
268    }
269
270    /// Attach a speed limit in km/h.
271    pub fn with_speed_limit_kph(mut self, speed_limit_kph: f64) -> Self {
272        self.speed_limit_kph = Some(speed_limit_kph);
273        self
274    }
275}
276
277impl SemanticEntity for TransportLinkMetadata {
278    type Id = EdgeId;
279
280    fn semantic_id(&self) -> Self::Id {
281        self.edge_id
282    }
283}
284
285/// Minimal reusable metadata for a transit stop/platform anchor.
286#[derive(Debug, Clone, Copy, PartialEq)]
287pub struct TransitStopMetadata {
288    /// Stable topology node identifier for the stop anchor.
289    pub node_id: NodeId,
290    /// Optional semantic zone containing the stop.
291    pub zone_id: Option<ZoneId>,
292    /// Optional level relation for the stop.
293    pub level_relation: Option<LevelRelation>,
294    /// Modes served or admitted at the stop.
295    pub served_modes: AllowedModes,
296}
297
298impl TransitStopMetadata {
299    /// Create stop metadata for a node.
300    pub fn new(node_id: NodeId, served_modes: AllowedModes) -> Self {
301        Self {
302            node_id,
303            zone_id: None,
304            level_relation: None,
305            served_modes,
306        }
307    }
308
309    /// Attach a containing semantic zone.
310    pub fn with_zone(mut self, zone_id: ZoneId) -> Self {
311        self.zone_id = Some(zone_id);
312        self
313    }
314
315    /// Attach a level relation.
316    pub fn with_level_relation(mut self, level_relation: LevelRelation) -> Self {
317        self.level_relation = Some(level_relation);
318        self
319    }
320}
321
322impl SemanticEntity for TransitStopMetadata {
323    type Id = NodeId;
324
325    fn semantic_id(&self) -> Self::Id {
326        self.node_id
327    }
328}
329
330/// Canonical transport-specialized link space.
331pub type TransportLinkSpace = LinkSpace<LinkProperties>;
332
333/// Transport-specific operations over `LinkSpace<LinkProperties>`.
334///
335/// These helpers define a small reusable baseline policy layer.
336/// They are suitable for simple transport scenarios and examples, but they do
337/// not claim calibrated or reference-grade operational realism.
338pub trait TransportLinkOps {
339    /// Per-link density computed from current occupancy, link length, and lane count.
340    fn link_density(&self, link_id: LinkId) -> f64;
341    /// Per-link snapshot speed computed from the configured fundamental diagram.
342    fn link_speed(&self, link_id: LinkId) -> f64;
343    /// Free-flow traversal time using static geometry and free-flow speed.
344    fn link_free_flow_time(&self, link_id: LinkId) -> f64;
345    /// Snapshot traversal time using current `link_speed`.
346    fn link_travel_time(&self, link_id: LinkId) -> f64;
347    /// Simple FIFO/gap-limited speed estimate for one agent on its current link.
348    fn agent_speed(&self, id: u64) -> Result<f64, LinkSpaceError>;
349    /// Speed decision for one agent using an explicit queue policy.
350    fn agent_speed_decision<P: QueuePolicy>(
351        &self,
352        id: u64,
353        policy: &P,
354    ) -> Result<SpeedDecision, LinkSpaceError>;
355    /// Speed estimate for one agent using an explicit queue policy.
356    fn agent_speed_with_policy<P: QueuePolicy>(
357        &self,
358        id: u64,
359        policy: &P,
360    ) -> Result<f64, LinkSpaceError>;
361    /// Simple volume/capacity ratio over an externally supplied time window.
362    fn volume_capacity_ratio(&self, link_id: LinkId, time_window_s: f64) -> f64;
363}
364
365impl TransportLinkOps for LinkSpace<LinkProperties> {
366    fn link_density(&self, link_id: LinkId) -> f64 {
367        let Some(props) = self.link_properties(link_id) else {
368            return 0.0;
369        };
370        let Some(length) = self.link_length(link_id) else {
371            return 0.0;
372        };
373        let n = self.agents_on_link(link_id) as f64;
374        let length_km = length / 1000.0;
375        let lanes = props.lanes.max(1) as f64;
376        if length_km <= 0.0 {
377            return 0.0;
378        }
379        n / (length_km * lanes)
380    }
381
382    fn link_speed(&self, link_id: LinkId) -> f64 {
383        let Some(props) = self.link_properties(link_id) else {
384            return 0.0;
385        };
386        props.diagram.speed(
387            self.link_density(link_id),
388            props.free_flow_speed,
389            props.jam_density,
390        )
391    }
392
393    fn link_free_flow_time(&self, link_id: LinkId) -> f64 {
394        let Some(props) = self.link_properties(link_id) else {
395            return f64::INFINITY;
396        };
397        let Some(length) = self.link_length(link_id) else {
398            return f64::INFINITY;
399        };
400        length / props.free_flow_speed
401    }
402
403    fn link_travel_time(&self, link_id: LinkId) -> f64 {
404        let speed = self.link_speed(link_id);
405        if speed <= 0.0 {
406            return f64::INFINITY;
407        }
408        let Some(length) = self.link_length(link_id) else {
409            return f64::INFINITY;
410        };
411        length / speed
412    }
413
414    fn agent_speed(&self, id: u64) -> Result<f64, LinkSpaceError> {
415        self.agent_speed_with_policy(id, &FifoGapPolicy::default())
416    }
417
418    fn agent_speed_decision<P: QueuePolicy>(
419        &self,
420        id: u64,
421        policy: &P,
422    ) -> Result<SpeedDecision, LinkSpaceError> {
423        policy.speed_for(self, id)
424    }
425
426    fn agent_speed_with_policy<P: QueuePolicy>(
427        &self,
428        id: u64,
429        policy: &P,
430    ) -> Result<f64, LinkSpaceError> {
431        Ok(self.agent_speed_decision(id, policy)?.speed)
432    }
433
434    fn volume_capacity_ratio(&self, link_id: LinkId, time_window_s: f64) -> f64 {
435        let Some(props) = self.link_properties(link_id) else {
436            return 0.0;
437        };
438        let count = self.agents_on_link(link_id) as f64;
439        let flow_rate = count / (time_window_s / 3600.0);
440        flow_rate / props.capacity
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use rustsim_modes::TravelMode;
448
449    #[test]
450    fn transport_link_metadata_builder_helpers() {
451        let link = TransportLinkMetadata::new(7, 1, 2, LinkClass::Road, AllowedModes::vehicular())
452            .with_traffic_control(TrafficControlType::Signal)
453            .with_turn_type(TurnType::Through)
454            .with_speed_limit_kph(50.0);
455
456        assert_eq!(link.semantic_id(), 7);
457        assert_eq!(link.from_node, 1);
458        assert_eq!(link.to_node, 2);
459        assert_eq!(link.link_class, LinkClass::Road);
460        assert!(link.allowed_modes.allows(TravelMode::Vehicle));
461        assert!(link.allowed_modes.allows(TravelMode::Transit));
462        assert_eq!(link.traffic_control, Some(TrafficControlType::Signal));
463        assert_eq!(link.turn_type, Some(TurnType::Through));
464        assert_eq!(link.speed_limit_kph, Some(50.0));
465    }
466
467    #[test]
468    fn transit_stop_metadata_builder_helpers() {
469        let stop = TransitStopMetadata::new(3, AllowedModes::none().with_mode(TravelMode::Transit))
470            .with_zone(10)
471            .with_level_relation(LevelRelation::on(2));
472
473        assert_eq!(stop.semantic_id(), 3);
474        assert_eq!(stop.zone_id, Some(10));
475        assert_eq!(stop.level_relation, Some(LevelRelation::on(2)));
476        assert!(stop.served_modes.allows(TravelMode::Transit));
477    }
478
479    #[test]
480    fn fundamental_diagram_and_link_ops_work() {
481        let fd = FundamentalDiagram::Greenshields;
482        assert!((fd.speed(75.0, 13.9, 150.0) - 6.95).abs() < 0.1);
483
484        let mut space: TransportLinkSpace = LinkSpace::new();
485        let a = space.add_node();
486        let b = space.add_node();
487        let (geom, props) = LinkProperties::urban(500.0, 50.0, 2).unwrap();
488        let link = space.add_link(a, b, geom, props).unwrap();
489        for i in 1..=15 {
490            space.add_agent_to_link(i, link, (i as f64) * 30.0).unwrap();
491        }
492
493        assert!((space.link_density(link) - 15.0).abs() < 1e-6);
494        assert!((space.link_speed(link) - 12.51).abs() < 0.1);
495        let ff_time = space.link_free_flow_time(link);
496        assert!((ff_time - 36.0).abs() < 1.0);
497        assert!(space.volume_capacity_ratio(link, 3600.0) > 0.0);
498    }
499}