Skip to main content

rustsim_spaces/
link.rs

1//! Generic link-based network space.
2//!
3//! [`LinkSpace<P>`] represents a directed network where agents move along links
4//! (edges) rather than residing at nodes. The space is intentionally domain-
5//! neutral: it models topology, FIFO occupancy, link blocking, and agent
6//! transfer between links, while parameterizing per-link semantics via `P`.
7//!
8//! Transport-specific semantics such as speed-density relationships and lane
9//! counts live in `rustsim-traffic` and can use `LinkSpace<LinkProperties>`.
10
11use rustsim_core::{
12    space::Space,
13    types::{AgentId, EdgeId, NodeId},
14};
15use std::collections::HashMap;
16use thiserror::Error;
17
18/// Unique identifier for a link (directed edge) in the network.
19pub type LinkId = EdgeId;
20
21/// Errors returned by link-geometry validation.
22#[derive(Debug, Clone, Copy, PartialEq, Error)]
23pub enum LinkGeometryError {
24    /// Link length must be strictly positive.
25    #[error("link length must be positive")]
26    InvalidLength,
27}
28
29/// Minimal geometric properties of a generic link.
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct LinkGeometry {
32    /// Link length in meters.
33    pub length: f64,
34}
35
36impl LinkGeometry {
37    /// Create generic link geometry from a length in meters.
38    pub fn new(length: f64) -> Result<Self, LinkGeometryError> {
39        if length <= 0.0 {
40            return Err(LinkGeometryError::InvalidLength);
41        }
42        Ok(Self { length })
43    }
44}
45
46/// Errors returned by link space operations.
47#[derive(Debug, Clone, PartialEq, Error)]
48pub enum LinkSpaceError {
49    /// The node index is out of range.
50    #[error("invalid node index {0}")]
51    InvalidNode(NodeId),
52    /// The link index is out of range.
53    #[error("invalid link index {0}")]
54    InvalidLink(LinkId),
55    /// Agent is not on any link.
56    #[error("agent {0} not found on any link")]
57    AgentNotFound(AgentId),
58    /// Agent already exists on a link.
59    #[error("agent {0} already exists on link {1}")]
60    AgentAlreadyOnLink(AgentId, LinkId),
61}
62
63#[derive(Debug, Clone, Copy)]
64struct AgentOnLink {
65    link_id: LinkId,
66    position: f64,
67}
68
69/// Internal link data.
70#[derive(Debug, Clone)]
71struct LinkData<P> {
72    from: NodeId,
73    to: NodeId,
74    geometry: LinkGeometry,
75    properties: P,
76    agents: Vec<AgentId>,
77    exit_blocked: bool,
78}
79
80/// Generic link-based network space.
81#[derive(Debug, Clone)]
82pub struct LinkSpace<P = ()> {
83    nodes: Vec<NodeData>,
84    links: Vec<LinkData<P>>,
85    agent_state: HashMap<AgentId, AgentOnLink>,
86}
87
88#[derive(Debug, Clone, Default)]
89struct NodeData {
90    outgoing: Vec<LinkId>,
91    incoming: Vec<LinkId>,
92}
93
94impl<P> LinkSpace<P> {
95    /// Create an empty link space with no nodes or links.
96    pub fn new() -> Self {
97        Self {
98            nodes: Vec::new(),
99            links: Vec::new(),
100            agent_state: HashMap::new(),
101        }
102    }
103
104    /// Number of nodes in the network.
105    pub fn num_nodes(&self) -> usize {
106        self.nodes.len()
107    }
108
109    /// Number of links in the network.
110    pub fn num_links(&self) -> usize {
111        self.links.len()
112    }
113
114    /// Add a node and return its index.
115    pub fn add_node(&mut self) -> NodeId {
116        let id = self.nodes.len();
117        self.nodes.push(NodeData::default());
118        id
119    }
120
121    /// Add a directed link from `from` to `to` with geometry and per-link properties.
122    pub fn add_link(
123        &mut self,
124        from: NodeId,
125        to: NodeId,
126        geometry: LinkGeometry,
127        properties: P,
128    ) -> Result<LinkId, LinkSpaceError> {
129        if from >= self.nodes.len() {
130            return Err(LinkSpaceError::InvalidNode(from));
131        }
132        if to >= self.nodes.len() {
133            return Err(LinkSpaceError::InvalidNode(to));
134        }
135
136        let link_id = self.links.len();
137        self.links.push(LinkData {
138            from,
139            to,
140            geometry,
141            properties,
142            agents: Vec::new(),
143            exit_blocked: false,
144        });
145        self.nodes[from].outgoing.push(link_id);
146        self.nodes[to].incoming.push(link_id);
147        Ok(link_id)
148    }
149
150    /// Immutable access to link geometry.
151    pub fn link_geometry(&self, link_id: LinkId) -> Option<&LinkGeometry> {
152        self.links.get(link_id).map(|l| &l.geometry)
153    }
154
155    /// Mutable access to link geometry.
156    pub fn link_geometry_mut(&mut self, link_id: LinkId) -> Option<&mut LinkGeometry> {
157        self.links.get_mut(link_id).map(|l| &mut l.geometry)
158    }
159
160    /// Immutable access to per-link properties.
161    pub fn link_properties(&self, link_id: LinkId) -> Option<&P> {
162        self.links.get(link_id).map(|l| &l.properties)
163    }
164
165    /// Mutable access to per-link properties.
166    pub fn link_properties_mut(&mut self, link_id: LinkId) -> Option<&mut P> {
167        self.links.get_mut(link_id).map(|l| &mut l.properties)
168    }
169
170    /// Source and destination nodes of a link as `(from, to)`.
171    pub fn link_endpoints(&self, link_id: LinkId) -> Option<(NodeId, NodeId)> {
172        self.links.get(link_id).map(|l| (l.from, l.to))
173    }
174
175    /// Outgoing link IDs from a node.
176    pub fn outgoing_links(&self, node: NodeId) -> &[LinkId] {
177        &self.nodes[node].outgoing
178    }
179
180    /// Incoming link IDs to a node.
181    pub fn incoming_links(&self, node: NodeId) -> &[LinkId] {
182        &self.nodes[node].incoming
183    }
184
185    /// Number of agents currently on a link.
186    pub fn agents_on_link(&self, link_id: LinkId) -> usize {
187        self.links.get(link_id).map_or(0, |l| l.agents.len())
188    }
189
190    /// Agent IDs currently on a link, ordered by position (upstream first).
191    pub fn agent_ids_on_link(&self, link_id: LinkId) -> &[AgentId] {
192        self.links
193            .get(link_id)
194            .map_or(&[] as &[AgentId], |l| &l.agents)
195    }
196
197    /// Link length in meters.
198    pub fn link_length(&self, link_id: LinkId) -> Option<f64> {
199        self.link_geometry(link_id).map(|g| g.length)
200    }
201
202    /// Set whether a link's exit is blocked by downstream logic.
203    pub fn set_link_exit_blocked(&mut self, link_id: LinkId, blocked: bool) {
204        if let Some(link) = self.links.get_mut(link_id) {
205            link.exit_blocked = blocked;
206        }
207    }
208
209    /// Whether a link's exit is currently blocked.
210    pub fn link_exit_blocked(&self, link_id: LinkId) -> bool {
211        self.links.get(link_id).is_some_and(|l| l.exit_blocked)
212    }
213
214    /// Get an agent's current link and position.
215    pub fn agent_position(&self, id: AgentId) -> Option<(LinkId, f64)> {
216        self.agent_state.get(&id).map(|s| (s.link_id, s.position))
217    }
218
219    /// Place an agent on a link at a specific position.
220    pub fn add_agent_to_link(
221        &mut self,
222        id: AgentId,
223        link_id: LinkId,
224        position: f64,
225    ) -> Result<(), LinkSpaceError> {
226        if link_id >= self.links.len() {
227            return Err(LinkSpaceError::InvalidLink(link_id));
228        }
229        if self.agent_state.contains_key(&id) {
230            return Err(LinkSpaceError::AgentAlreadyOnLink(id, link_id));
231        }
232
233        let length = self.links[link_id].geometry.length;
234        let pos = position.clamp(0.0, length);
235
236        self.agent_state.insert(
237            id,
238            AgentOnLink {
239                link_id,
240                position: pos,
241            },
242        );
243
244        let link = &mut self.links[link_id];
245        let insert_idx = link
246            .agents
247            .iter()
248            .position(|&aid| match self.agent_state.get(&aid) {
249                None => true,
250                Some(state) => state.position > pos,
251            })
252            .unwrap_or(link.agents.len());
253        link.agents.insert(insert_idx, id);
254
255        Ok(())
256    }
257
258    /// Remove an agent from the network.
259    pub fn remove_agent(&mut self, id: AgentId) -> Result<(), LinkSpaceError> {
260        let state = self
261            .agent_state
262            .remove(&id)
263            .ok_or(LinkSpaceError::AgentNotFound(id))?;
264        let link = &mut self.links[state.link_id];
265        if let Some(i) = link.agents.iter().position(|&a| a == id) {
266            link.agents.remove(i);
267        }
268        Ok(())
269    }
270
271    /// Advance an agent's position on its current link by `distance` meters.
272    pub fn advance_agent(&mut self, id: AgentId, distance: f64) -> Result<f64, LinkSpaceError> {
273        let state = self
274            .agent_state
275            .get(&id)
276            .copied()
277            .ok_or(LinkSpaceError::AgentNotFound(id))?;
278
279        let link = &self.links[state.link_id];
280        let length = link.geometry.length;
281        let my_idx = link.agents.iter().position(|&a| a == id).expect(
282            "invariant: agent present in agent_state must also appear on its link's agent list",
283        );
284
285        let max_pos = if my_idx + 1 < link.agents.len() {
286            let ahead_id = link.agents[my_idx + 1];
287            let ahead_pos = self.agent_state[&ahead_id].position;
288            (ahead_pos - 5.0).max(state.position)
289        } else {
290            length
291        };
292
293        let new_pos = (state.position + distance).min(max_pos).min(length);
294        self.agent_state
295            .get_mut(&id)
296            .expect("invariant: agent existed at function entry and has not been removed")
297            .position = new_pos;
298        Ok(new_pos)
299    }
300
301    /// Check if an agent has reached the downstream end of its link.
302    pub fn agent_at_link_end(&self, id: AgentId) -> bool {
303        if let Some(state) = self.agent_state.get(&id) {
304            let length = self.links[state.link_id].geometry.length;
305            state.position >= length - 0.01
306        } else {
307            false
308        }
309    }
310
311    /// Transfer an agent from the end of its current link to the start of a new link.
312    pub fn transfer_agent(&mut self, id: AgentId, to_link: LinkId) -> Result<(), LinkSpaceError> {
313        let state = self
314            .agent_state
315            .get(&id)
316            .copied()
317            .ok_or(LinkSpaceError::AgentNotFound(id))?;
318
319        if to_link >= self.links.len() {
320            return Err(LinkSpaceError::InvalidLink(to_link));
321        }
322
323        let current_to = self.links[state.link_id].to;
324        let next_from = self.links[to_link].from;
325
326        if current_to != next_from {
327            return Err(LinkSpaceError::InvalidLink(to_link));
328        }
329
330        let old_link = &mut self.links[state.link_id];
331        if let Some(i) = old_link.agents.iter().position(|&a| a == id) {
332            old_link.agents.remove(i);
333        }
334
335        let entry = self
336            .agent_state
337            .get_mut(&id)
338            .expect("invariant: agent existed at function entry and has not been removed");
339        entry.link_id = to_link;
340        entry.position = 0.0;
341
342        let new_link = &mut self.links[to_link];
343        new_link.agents.insert(0, id);
344
345        Ok(())
346    }
347
348    /// Total number of agents in the network.
349    pub fn total_agents(&self) -> usize {
350        self.agent_state.len()
351    }
352
353    /// All agent IDs in the network.
354    pub fn all_agent_ids(&self) -> Vec<AgentId> {
355        self.agent_state.keys().copied().collect()
356    }
357}
358
359impl<P> Default for LinkSpace<P> {
360    fn default() -> Self {
361        Self::new()
362    }
363}
364
365impl<P: Send + Sync> Space for LinkSpace<P> {}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    fn simple_network() -> LinkSpace<()> {
372        let mut space = LinkSpace::new();
373        let a = space.add_node();
374        let b = space.add_node();
375        let c = space.add_node();
376        space
377            .add_link(a, b, LinkGeometry::new(500.0).unwrap(), ())
378            .unwrap();
379        space
380            .add_link(b, c, LinkGeometry::new(300.0).unwrap(), ())
381            .unwrap();
382        space
383    }
384
385    #[test]
386    fn network_topology() {
387        let space = simple_network();
388        assert_eq!(space.num_nodes(), 3);
389        assert_eq!(space.num_links(), 2);
390        assert_eq!(space.link_endpoints(0), Some((0, 1)));
391        assert_eq!(space.link_endpoints(1), Some((1, 2)));
392        assert_eq!(space.outgoing_links(0), &[0]);
393        assert_eq!(space.incoming_links(1), &[0]);
394        assert_eq!(space.link_length(0), Some(500.0));
395    }
396
397    #[test]
398    fn add_and_remove_agent() {
399        let mut space = simple_network();
400        space.add_agent_to_link(1, 0, 100.0).unwrap();
401
402        assert_eq!(space.agents_on_link(0), 1);
403        assert_eq!(space.agent_position(1), Some((0, 100.0)));
404
405        space.remove_agent(1).unwrap();
406        assert_eq!(space.agents_on_link(0), 0);
407        assert!(space.agent_position(1).is_none());
408    }
409
410    #[test]
411    fn advance_agent_basic() {
412        let mut space = simple_network();
413        space.add_agent_to_link(1, 0, 0.0).unwrap();
414
415        let new_pos = space.advance_agent(1, 50.0).unwrap();
416        assert!((new_pos - 50.0).abs() < 1e-9);
417
418        let new_pos = space.advance_agent(1, 1000.0).unwrap();
419        assert!((new_pos - 500.0).abs() < 1e-9);
420    }
421
422    #[test]
423    fn fifo_ordering() {
424        let mut space = simple_network();
425        space.add_agent_to_link(1, 0, 100.0).unwrap();
426        space.add_agent_to_link(2, 0, 200.0).unwrap();
427
428        let new_pos = space.advance_agent(1, 200.0).unwrap();
429        assert!(new_pos <= 195.0 + 1e-9);
430    }
431
432    #[test]
433    fn transfer_agent() {
434        let mut space = simple_network();
435        space.add_agent_to_link(1, 0, 490.0).unwrap();
436
437        space.advance_agent(1, 100.0).unwrap();
438        assert!(space.agent_at_link_end(1));
439
440        space.transfer_agent(1, 1).unwrap();
441        assert_eq!(space.agent_position(1), Some((1, 0.0)));
442        assert_eq!(space.agents_on_link(0), 0);
443        assert_eq!(space.agents_on_link(1), 1);
444    }
445
446    #[test]
447    fn exit_blocked_flag() {
448        let mut space = simple_network();
449        assert!(!space.link_exit_blocked(0));
450        space.set_link_exit_blocked(0, true);
451        assert!(space.link_exit_blocked(0));
452    }
453
454    #[test]
455    fn agent_already_on_link_error() {
456        let mut space = simple_network();
457        space.add_agent_to_link(1, 0, 0.0).unwrap();
458        let err = space.add_agent_to_link(1, 0, 50.0).unwrap_err();
459        assert_eq!(err, LinkSpaceError::AgentAlreadyOnLink(1, 0));
460    }
461
462    #[test]
463    fn agent_not_found_error() {
464        let mut space = simple_network();
465        let err = space.advance_agent(99, 10.0).unwrap_err();
466        assert_eq!(err, LinkSpaceError::AgentNotFound(99));
467    }
468}