scirs2_graph/attributed_graph.rs
1//! Attributed graphs with typed node and edge features
2//!
3//! This module provides `AttributedGraph<N, E>` – a graph where every node
4//! carries data of type `N` and every edge carries data of type `E`. It
5//! differs from the attribute-map approach in [`crate::attributes`] in that
6//! the feature types are *statically known*, enabling zero-cost extraction of
7//! dense feature matrices via a user-supplied projection function.
8//!
9//! ## Key types
10//!
11//! | Type | Purpose |
12//! |------|---------|
13//! [`AttributedGraph<N,E>`] | Core graph container |
14//! [`AttributedGraphBuilder<N,E>`] | Ergonomic builder |
15//! [`NeighborInfo<N,E>`] | Neighbour + data bundle returned by [`attributed_neighbors`] |
16//!
17//! ## Free functions
18//!
19//! - [`node_feature_matrix`] – project node data into an `Array2<f64>`
20//! - [`edge_feature_matrix`] – project edge data into an `Array2<f64>`
21//! - [`attributed_neighbors`] – enumerate neighbours with their data
22//! - [`dijkstra_attributed`] – generalised Dijkstra using a caller-supplied cost function
23
24use std::collections::HashMap;
25
26use scirs2_core::ndarray::Array2;
27
28use crate::error::{GraphError, Result};
29
30// ============================================================================
31// NodeId
32// ============================================================================
33
34/// Stable integer node identifier.
35///
36/// Assigned sequentially by [`AttributedGraph::add_node`]. Remains valid as
37/// long as the graph is not cleared.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct NodeId(pub usize);
40
41impl std::fmt::Display for NodeId {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(f, "NodeId({})", self.0)
44 }
45}
46
47// ============================================================================
48// AttributedGraph
49// ============================================================================
50
51/// A directed graph whose nodes carry data of type `N` and whose edges carry
52/// data of type `E`.
53///
54/// Internally, the graph is represented as an adjacency list over [`NodeId`]
55/// indices. Parallel edges (same source and target) are supported; the first
56/// matching edge is returned by lookup helpers.
57///
58/// # Example
59///
60/// ```
61/// use scirs2_graph::attributed_graph::{AttributedGraph, NodeId};
62///
63/// #[derive(Debug, Clone)]
64/// struct Person { name: String, age: u32 }
65///
66/// #[derive(Debug, Clone)]
67/// struct Knows { since: u32 }
68///
69/// let mut g: AttributedGraph<Person, Knows> = AttributedGraph::new();
70/// let alice = g.add_node(Person { name: "Alice".into(), age: 30 });
71/// let bob = g.add_node(Person { name: "Bob".into(), age: 25 });
72/// g.add_edge(alice, bob, Knows { since: 2020 }).unwrap();
73///
74/// assert_eq!(g.node_count(), 2);
75/// assert_eq!(g.edge_count(), 1);
76/// ```
77#[derive(Debug, Clone)]
78pub struct AttributedGraph<N, E> {
79 /// Node data indexed by internal position (= NodeId.0)
80 nodes: Vec<N>,
81 /// Adjacency list: `adj[src_idx]` holds `(target_idx, edge_idx)` pairs
82 adj: Vec<Vec<(usize, usize)>>,
83 /// Edge storage: `(src_idx, dst_idx, E)`
84 edges: Vec<(usize, usize, E)>,
85}
86
87impl<N, E> Default for AttributedGraph<N, E> {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl<N, E> AttributedGraph<N, E> {
94 /// Create an empty attributed graph.
95 pub fn new() -> Self {
96 Self {
97 nodes: Vec::new(),
98 adj: Vec::new(),
99 edges: Vec::new(),
100 }
101 }
102
103 /// Create an empty graph with pre-allocated capacity.
104 pub fn with_capacity(node_capacity: usize, edge_capacity: usize) -> Self {
105 Self {
106 nodes: Vec::with_capacity(node_capacity),
107 adj: Vec::with_capacity(node_capacity),
108 edges: Vec::with_capacity(edge_capacity),
109 }
110 }
111
112 // -----------------------------------------------------------------------
113 // Mutation
114 // -----------------------------------------------------------------------
115
116 /// Add a node with the given data; returns its [`NodeId`].
117 pub fn add_node(&mut self, data: N) -> NodeId {
118 let id = self.nodes.len();
119 self.nodes.push(data);
120 self.adj.push(Vec::new());
121 NodeId(id)
122 }
123
124 /// Add a directed edge from `src` to `dst` with data `edge_data`.
125 ///
126 /// Both nodes must already exist.
127 ///
128 /// # Errors
129 ///
130 /// Returns [`GraphError::NodeNotFound`] if either node is absent.
131 pub fn add_edge(&mut self, src: NodeId, dst: NodeId, edge_data: E) -> Result<()> {
132 self.validate_node(src)?;
133 self.validate_node(dst)?;
134 let edge_idx = self.edges.len();
135 self.edges.push((src.0, dst.0, edge_data));
136 self.adj[src.0].push((dst.0, edge_idx));
137 Ok(())
138 }
139
140 // -----------------------------------------------------------------------
141 // Query
142 // -----------------------------------------------------------------------
143
144 /// Number of nodes.
145 #[inline]
146 pub fn node_count(&self) -> usize {
147 self.nodes.len()
148 }
149
150 /// Number of (directed) edges.
151 #[inline]
152 pub fn edge_count(&self) -> usize {
153 self.edges.len()
154 }
155
156 /// Retrieve node data by id.
157 ///
158 /// Returns `None` if the id is out of range.
159 pub fn node_data(&self, id: NodeId) -> Option<&N> {
160 self.nodes.get(id.0)
161 }
162
163 /// Retrieve mutable node data by id.
164 pub fn node_data_mut(&mut self, id: NodeId) -> Option<&mut N> {
165 self.nodes.get_mut(id.0)
166 }
167
168 /// Iterate over all node ids and their data.
169 pub fn nodes(&self) -> impl Iterator<Item = (NodeId, &N)> {
170 self.nodes.iter().enumerate().map(|(i, n)| (NodeId(i), n))
171 }
172
173 /// Iterate over all edges as `(src, dst, &E)`.
174 pub fn edges_iter(&self) -> impl Iterator<Item = (NodeId, NodeId, &E)> {
175 self.edges
176 .iter()
177 .map(|(s, d, e)| (NodeId(*s), NodeId(*d), e))
178 }
179
180 /// Return the direct out-neighbours of `node` as `(target, &E)` pairs.
181 ///
182 /// # Errors
183 ///
184 /// Returns [`GraphError::NodeNotFound`] if the node is absent.
185 pub fn out_neighbors(&self, node: NodeId) -> Result<Vec<(NodeId, &E)>> {
186 self.validate_node(node)?;
187 let result = self.adj[node.0]
188 .iter()
189 .map(|&(dst, eidx)| (NodeId(dst), &self.edges[eidx].2))
190 .collect();
191 Ok(result)
192 }
193
194 /// Return whether an edge from `src` to `dst` exists.
195 pub fn has_edge(&self, src: NodeId, dst: NodeId) -> bool {
196 if src.0 >= self.nodes.len() || dst.0 >= self.nodes.len() {
197 return false;
198 }
199 self.adj[src.0].iter().any(|&(d, _)| d == dst.0)
200 }
201
202 /// Retrieve the data of the first edge from `src` to `dst`.
203 ///
204 /// Returns `None` if no such edge exists.
205 pub fn edge_data(&self, src: NodeId, dst: NodeId) -> Option<&E> {
206 if src.0 >= self.nodes.len() {
207 return None;
208 }
209 self.adj[src.0]
210 .iter()
211 .find(|&&(d, _)| d == dst.0)
212 .map(|&(_, eidx)| &self.edges[eidx].2)
213 }
214
215 // -----------------------------------------------------------------------
216 // Internal helpers
217 // -----------------------------------------------------------------------
218
219 fn validate_node(&self, id: NodeId) -> Result<()> {
220 if id.0 < self.nodes.len() {
221 Ok(())
222 } else {
223 Err(GraphError::node_not_found_with_context(
224 id.0,
225 self.nodes.len(),
226 "AttributedGraph node validation",
227 ))
228 }
229 }
230}
231
232// ============================================================================
233// AttributedGraphBuilder
234// ============================================================================
235
236/// Builder for [`AttributedGraph`] that provides a fluent API.
237///
238/// # Example
239///
240/// ```
241/// use scirs2_graph::attributed_graph::{AttributedGraphBuilder, NodeId};
242///
243/// let mut builder: AttributedGraphBuilder<f64, f32> = AttributedGraphBuilder::new();
244/// let a = builder.node(1.0);
245/// let b = builder.node(2.0);
246/// builder.edge(a, b, 0.5_f32);
247/// let graph = builder.build().expect("build failed");
248/// assert_eq!(graph.node_count(), 2);
249/// assert_eq!(graph.edge_count(), 1);
250/// ```
251#[derive(Debug)]
252pub struct AttributedGraphBuilder<N, E> {
253 graph: AttributedGraph<N, E>,
254 /// Accumulated errors from `edge()` calls
255 errors: Vec<GraphError>,
256}
257
258impl<N, E> Default for AttributedGraphBuilder<N, E> {
259 fn default() -> Self {
260 Self::new()
261 }
262}
263
264impl<N, E> AttributedGraphBuilder<N, E> {
265 /// Create a new empty builder.
266 pub fn new() -> Self {
267 Self {
268 graph: AttributedGraph::new(),
269 errors: Vec::new(),
270 }
271 }
272
273 /// Create a builder with capacity hints.
274 pub fn with_capacity(node_capacity: usize, edge_capacity: usize) -> Self {
275 Self {
276 graph: AttributedGraph::with_capacity(node_capacity, edge_capacity),
277 errors: Vec::new(),
278 }
279 }
280
281 /// Add a node; returns the assigned [`NodeId`].
282 pub fn node(&mut self, data: N) -> NodeId {
283 self.graph.add_node(data)
284 }
285
286 /// Add a directed edge. Any error is deferred until [`build`][Self::build].
287 pub fn edge(&mut self, src: NodeId, dst: NodeId, data: E) -> &mut Self {
288 if let Err(e) = self.graph.add_edge(src, dst, data) {
289 self.errors.push(e);
290 }
291 self
292 }
293
294 /// Consume the builder and return the graph.
295 ///
296 /// # Errors
297 ///
298 /// Returns the first deferred error, if any.
299 pub fn build(self) -> Result<AttributedGraph<N, E>> {
300 if let Some(err) = self.errors.into_iter().next() {
301 return Err(err);
302 }
303 Ok(self.graph)
304 }
305
306 /// Consume the builder, returning the graph without checking errors.
307 ///
308 /// Silently drops any accumulated errors.
309 pub fn build_unchecked(self) -> AttributedGraph<N, E> {
310 self.graph
311 }
312}
313
314// ============================================================================
315// NeighborInfo
316// ============================================================================
317
318/// A neighbour together with its node data and the connecting edge data.
319///
320/// Returned by [`attributed_neighbors`].
321#[derive(Debug)]
322pub struct NeighborInfo<'a, N, E> {
323 /// The neighbour's id.
324 pub id: NodeId,
325 /// Reference to the neighbour's node data.
326 pub node_data: &'a N,
327 /// Reference to the edge data connecting source → neighbour.
328 pub edge_data: &'a E,
329}
330
331// ============================================================================
332// Free functions
333// ============================================================================
334
335/// Construct a dense node-feature matrix from an attributed graph.
336///
337/// `feature_fn` is called for each node in insertion order and must return a
338/// `Vec<f64>` of the same length for every node. The resulting matrix has
339/// shape `(node_count, feature_dim)`.
340///
341/// # Errors
342///
343/// Returns [`GraphError::InvalidParameter`] if the node count is zero or if
344/// `feature_fn` returns vectors of inconsistent length.
345///
346/// # Example
347///
348/// ```
349/// use scirs2_graph::attributed_graph::{AttributedGraph, node_feature_matrix};
350///
351/// let mut g = AttributedGraph::<[f64; 2], ()>::new();
352/// g.add_node([1.0, 0.5]);
353/// g.add_node([0.0, 1.0]);
354/// let mat = node_feature_matrix(&g, |n| n.to_vec()).unwrap();
355/// assert_eq!(mat.shape(), &[2, 2]);
356/// ```
357pub fn node_feature_matrix<N, E, F>(
358 graph: &AttributedGraph<N, E>,
359 feature_fn: F,
360) -> Result<Array2<f64>>
361where
362 F: Fn(&N) -> Vec<f64>,
363{
364 let n = graph.node_count();
365 if n == 0 {
366 return Err(GraphError::invalid_parameter(
367 "graph",
368 "empty graph",
369 "at least one node",
370 ));
371 }
372
373 // Compute features for all nodes
374 let features: Vec<Vec<f64>> = graph.nodes.iter().map(feature_fn).collect();
375
376 let dim = features[0].len();
377 if dim == 0 {
378 return Err(GraphError::invalid_parameter(
379 "feature_fn",
380 "zero-length feature vector",
381 "non-empty feature vector",
382 ));
383 }
384
385 // Validate uniform dimensionality
386 for (i, fv) in features.iter().enumerate() {
387 if fv.len() != dim {
388 return Err(GraphError::InvalidParameter {
389 param: "feature_fn".to_string(),
390 value: format!("node {i} returned dim={}", fv.len()),
391 expected: format!("uniform dim={dim}"),
392 context: "node_feature_matrix".to_string(),
393 });
394 }
395 }
396
397 let mut mat = Array2::zeros((n, dim));
398 for (i, fv) in features.iter().enumerate() {
399 for (j, &v) in fv.iter().enumerate() {
400 mat[[i, j]] = v;
401 }
402 }
403 Ok(mat)
404}
405
406/// Construct a dense edge-feature matrix from an attributed graph.
407///
408/// `feature_fn` is called for each edge in insertion order and must return a
409/// `Vec<f64>` of the same length for every edge. The resulting matrix has
410/// shape `(edge_count, feature_dim)`.
411///
412/// # Errors
413///
414/// Returns [`GraphError::InvalidParameter`] if the edge count is zero or if
415/// `feature_fn` returns vectors of inconsistent length.
416pub fn edge_feature_matrix<N, E, F>(
417 graph: &AttributedGraph<N, E>,
418 feature_fn: F,
419) -> Result<Array2<f64>>
420where
421 F: Fn(&E) -> Vec<f64>,
422{
423 let m = graph.edge_count();
424 if m == 0 {
425 return Err(GraphError::invalid_parameter(
426 "graph",
427 "graph has no edges",
428 "at least one edge",
429 ));
430 }
431
432 let features: Vec<Vec<f64>> = graph.edges.iter().map(|(_, _, e)| feature_fn(e)).collect();
433
434 let dim = features[0].len();
435 if dim == 0 {
436 return Err(GraphError::invalid_parameter(
437 "feature_fn",
438 "zero-length feature vector",
439 "non-empty feature vector",
440 ));
441 }
442
443 for (i, fv) in features.iter().enumerate() {
444 if fv.len() != dim {
445 return Err(GraphError::InvalidParameter {
446 param: "feature_fn".to_string(),
447 value: format!("edge {i} returned dim={}", fv.len()),
448 expected: format!("uniform dim={dim}"),
449 context: "edge_feature_matrix".to_string(),
450 });
451 }
452 }
453
454 let mut mat = Array2::zeros((m, dim));
455 for (i, fv) in features.iter().enumerate() {
456 for (j, &v) in fv.iter().enumerate() {
457 mat[[i, j]] = v;
458 }
459 }
460 Ok(mat)
461}
462
463/// Return the out-neighbours of `node` together with their node data and the
464/// connecting edge data.
465///
466/// # Errors
467///
468/// Returns [`GraphError::NodeNotFound`] if `node` is not in the graph.
469///
470/// # Example
471///
472/// ```
473/// use scirs2_graph::attributed_graph::{AttributedGraph, attributed_neighbors};
474///
475/// let mut g = AttributedGraph::<&str, f64>::new();
476/// let a = g.add_node("Alice");
477/// let b = g.add_node("Bob");
478/// g.add_edge(a, b, 1.5).unwrap();
479///
480/// let nbrs = attributed_neighbors(a, &g).unwrap();
481/// assert_eq!(nbrs.len(), 1);
482/// assert_eq!(*nbrs[0].node_data, "Bob");
483/// assert_eq!(*nbrs[0].edge_data, 1.5);
484/// ```
485pub fn attributed_neighbors<'a, N, E>(
486 node: NodeId,
487 graph: &'a AttributedGraph<N, E>,
488) -> Result<Vec<NeighborInfo<'a, N, E>>> {
489 graph.validate_node(node)?;
490 let result = graph.adj[node.0]
491 .iter()
492 .map(|&(dst, eidx)| NeighborInfo {
493 id: NodeId(dst),
494 node_data: &graph.nodes[dst],
495 edge_data: &graph.edges[eidx].2,
496 })
497 .collect();
498 Ok(result)
499}
500
501// ============================================================================
502// Dijkstra
503// ============================================================================
504
505/// Priority queue entry used internally by Dijkstra.
506#[derive(Debug, Clone, PartialEq)]
507struct DijkstraEntry {
508 cost: f64,
509 node: usize,
510}
511
512impl Eq for DijkstraEntry {}
513
514impl PartialOrd for DijkstraEntry {
515 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
516 Some(self.cmp(other))
517 }
518}
519
520impl Ord for DijkstraEntry {
521 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
522 // Reversed for min-heap behaviour with BinaryHeap (max-heap)
523 other
524 .cost
525 .partial_cmp(&self.cost)
526 .unwrap_or(std::cmp::Ordering::Equal)
527 }
528}
529
530/// Generalised Dijkstra shortest-path algorithm on an attributed graph.
531///
532/// `cost_fn` is called for each edge and must return a non-negative `f64`
533/// cost. Negative costs will produce incorrect results (no check is
534/// performed for performance reasons).
535///
536/// Returns a map from each reachable [`NodeId`] to its minimum total cost
537/// from `source`. Unreachable nodes are absent from the map.
538///
539/// # Errors
540///
541/// Returns [`GraphError::NodeNotFound`] if `source` is not in the graph.
542///
543/// # Example
544///
545/// ```
546/// use scirs2_graph::attributed_graph::{AttributedGraph, dijkstra_attributed, NodeId};
547///
548/// let mut g = AttributedGraph::<(), f64>::new();
549/// let a = g.add_node(());
550/// let b = g.add_node(());
551/// let c = g.add_node(());
552/// g.add_edge(a, b, 2.0).unwrap();
553/// g.add_edge(b, c, 3.0).unwrap();
554/// g.add_edge(a, c, 10.0).unwrap();
555///
556/// let dist = dijkstra_attributed(&g, a, |e| *e).unwrap();
557/// assert_eq!(dist[&b], 2.0);
558/// assert_eq!(dist[&c], 5.0); // 2 + 3, not 10
559/// ```
560pub fn dijkstra_attributed<N, E, F>(
561 graph: &AttributedGraph<N, E>,
562 source: NodeId,
563 cost_fn: F,
564) -> Result<HashMap<NodeId, f64>>
565where
566 F: Fn(&E) -> f64,
567{
568 use std::collections::BinaryHeap;
569
570 graph.validate_node(source)?;
571
572 let n = graph.node_count();
573 let mut dist = vec![f64::INFINITY; n];
574 dist[source.0] = 0.0;
575
576 let mut heap = BinaryHeap::new();
577 heap.push(DijkstraEntry {
578 cost: 0.0,
579 node: source.0,
580 });
581
582 while let Some(DijkstraEntry { cost, node }) = heap.pop() {
583 // Skip stale entries
584 if cost > dist[node] {
585 continue;
586 }
587
588 for &(dst, eidx) in &graph.adj[node] {
589 let edge_cost = cost_fn(&graph.edges[eidx].2);
590 let new_cost = cost + edge_cost;
591 if new_cost < dist[dst] {
592 dist[dst] = new_cost;
593 heap.push(DijkstraEntry {
594 cost: new_cost,
595 node: dst,
596 });
597 }
598 }
599 }
600
601 let result = dist
602 .into_iter()
603 .enumerate()
604 .filter(|(_, d)| d.is_finite())
605 .map(|(i, d)| (NodeId(i), d))
606 .collect();
607
608 Ok(result)
609}
610
611// ============================================================================
612// Additional graph analysis functions
613// ============================================================================
614
615/// Compute the in-degree of each node.
616///
617/// Returns a `Vec<usize>` of length `graph.node_count()` where entry `i` is
618/// the number of edges *targeting* node `i`.
619pub fn in_degrees<N, E>(graph: &AttributedGraph<N, E>) -> Vec<usize> {
620 let mut deg = vec![0usize; graph.node_count()];
621 for (_, dst, _) in &graph.edges {
622 deg[*dst] += 1;
623 }
624 deg
625}
626
627/// Compute the out-degree of each node.
628///
629/// Returns a `Vec<usize>` of length `graph.node_count()`.
630pub fn out_degrees<N, E>(graph: &AttributedGraph<N, E>) -> Vec<usize> {
631 graph.adj.iter().map(|nbrs| nbrs.len()).collect()
632}
633
634/// Filter nodes by a predicate on their data, returning matching [`NodeId`]s.
635///
636/// # Example
637///
638/// ```
639/// use scirs2_graph::attributed_graph::{AttributedGraph, filter_nodes};
640///
641/// let mut g = AttributedGraph::<i32, ()>::new();
642/// g.add_node(10);
643/// g.add_node(20);
644/// g.add_node(30);
645///
646/// let big = filter_nodes(&g, |v| *v > 15);
647/// assert_eq!(big.len(), 2);
648/// ```
649pub fn filter_nodes<N, E, P>(graph: &AttributedGraph<N, E>, predicate: P) -> Vec<NodeId>
650where
651 P: Fn(&N) -> bool,
652{
653 graph
654 .nodes
655 .iter()
656 .enumerate()
657 .filter_map(|(i, n)| if predicate(n) { Some(NodeId(i)) } else { None })
658 .collect()
659}
660
661// ============================================================================
662// Tests
663// ============================================================================
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 // ------------------------------------------------------------------
670 // Helpers
671 // ------------------------------------------------------------------
672
673 #[derive(Debug, Clone, PartialEq)]
674 struct Person {
675 name: String,
676 age: u32,
677 }
678
679 #[derive(Debug, Clone, PartialEq)]
680 struct Rel {
681 weight: f64,
682 }
683
684 fn make_graph() -> AttributedGraph<Person, Rel> {
685 let mut g = AttributedGraph::new();
686 let alice = g.add_node(Person {
687 name: "Alice".into(),
688 age: 30,
689 });
690 let bob = g.add_node(Person {
691 name: "Bob".into(),
692 age: 25,
693 });
694 let charlie = g.add_node(Person {
695 name: "Charlie".into(),
696 age: 35,
697 });
698 g.add_edge(alice, bob, Rel { weight: 1.0 }).unwrap();
699 g.add_edge(bob, charlie, Rel { weight: 2.0 }).unwrap();
700 g.add_edge(alice, charlie, Rel { weight: 5.0 }).unwrap();
701 g
702 }
703
704 // ------------------------------------------------------------------
705 // Basic construction
706 // ------------------------------------------------------------------
707
708 #[test]
709 fn test_basic_construction() {
710 let g = make_graph();
711 assert_eq!(g.node_count(), 3);
712 assert_eq!(g.edge_count(), 3);
713 }
714
715 #[test]
716 fn test_node_data_access() {
717 let g = make_graph();
718 let alice = NodeId(0);
719 let data = g.node_data(alice).unwrap();
720 assert_eq!(data.name, "Alice");
721 assert_eq!(data.age, 30);
722 }
723
724 #[test]
725 fn test_edge_data_access() {
726 let g = make_graph();
727 let alice = NodeId(0);
728 let bob = NodeId(1);
729 let ed = g.edge_data(alice, bob).unwrap();
730 assert!((ed.weight - 1.0).abs() < 1e-12);
731 }
732
733 #[test]
734 fn test_has_edge() {
735 let g = make_graph();
736 assert!(g.has_edge(NodeId(0), NodeId(1)));
737 assert!(!g.has_edge(NodeId(1), NodeId(0)));
738 assert!(!g.has_edge(NodeId(2), NodeId(0)));
739 }
740
741 #[test]
742 fn test_invalid_node_add_edge() {
743 let mut g = AttributedGraph::<i32, ()>::new();
744 g.add_node(1);
745 let err = g.add_edge(NodeId(0), NodeId(5), ());
746 assert!(err.is_err());
747 }
748
749 // ------------------------------------------------------------------
750 // Builder
751 // ------------------------------------------------------------------
752
753 #[test]
754 fn test_builder() {
755 let mut b = AttributedGraphBuilder::<i32, f64>::new();
756 let a = b.node(1);
757 let bb = b.node(2);
758 b.edge(a, bb, std::f64::consts::PI);
759 let g = b.build().unwrap();
760 assert_eq!(g.node_count(), 2);
761 assert_eq!(g.edge_count(), 1);
762 }
763
764 #[test]
765 fn test_builder_bad_edge_deferred() {
766 let mut b = AttributedGraphBuilder::<i32, f64>::new();
767 b.node(1);
768 // NodeId(99) doesn't exist
769 b.edge(NodeId(0), NodeId(99), 1.0);
770 assert!(b.build().is_err());
771 }
772
773 // ------------------------------------------------------------------
774 // Feature matrices
775 // ------------------------------------------------------------------
776
777 #[test]
778 fn test_node_feature_matrix() {
779 let g = make_graph();
780 let mat = node_feature_matrix(&g, |p| vec![p.age as f64]).unwrap();
781 assert_eq!(mat.shape(), &[3, 1]);
782 assert!((mat[[0, 0]] - 30.0).abs() < 1e-12);
783 assert!((mat[[1, 0]] - 25.0).abs() < 1e-12);
784 assert!((mat[[2, 0]] - 35.0).abs() < 1e-12);
785 }
786
787 #[test]
788 fn test_edge_feature_matrix() {
789 let g = make_graph();
790 let mat = edge_feature_matrix(&g, |r| vec![r.weight]).unwrap();
791 assert_eq!(mat.shape(), &[3, 1]);
792 // Edges in insertion order: Alice→Bob(1.0), Bob→Charlie(2.0), Alice→Charlie(5.0)
793 assert!((mat[[0, 0]] - 1.0).abs() < 1e-12);
794 assert!((mat[[1, 0]] - 2.0).abs() < 1e-12);
795 assert!((mat[[2, 0]] - 5.0).abs() < 1e-12);
796 }
797
798 #[test]
799 fn test_node_feature_matrix_multi_dim() {
800 let g = make_graph();
801 let mat = node_feature_matrix(&g, |p| vec![p.age as f64, p.name.len() as f64]).unwrap();
802 assert_eq!(mat.shape(), &[3, 2]);
803 }
804
805 #[test]
806 fn test_node_feature_matrix_empty_graph() {
807 let g = AttributedGraph::<i32, ()>::new();
808 let result = node_feature_matrix(&g, |v| vec![*v as f64]);
809 assert!(result.is_err());
810 }
811
812 #[test]
813 fn test_edge_feature_matrix_no_edges() {
814 let mut g = AttributedGraph::<i32, f64>::new();
815 g.add_node(1);
816 let result = edge_feature_matrix(&g, |v| vec![*v]);
817 assert!(result.is_err());
818 }
819
820 // ------------------------------------------------------------------
821 // Attributed neighbours
822 // ------------------------------------------------------------------
823
824 #[test]
825 fn test_attributed_neighbors() {
826 let g = make_graph();
827 let nbrs = attributed_neighbors(NodeId(0), &g).unwrap();
828 assert_eq!(nbrs.len(), 2);
829 // Bob
830 assert_eq!(nbrs[0].node_data.name, "Bob");
831 assert!((nbrs[0].edge_data.weight - 1.0).abs() < 1e-12);
832 // Charlie (via direct edge)
833 assert_eq!(nbrs[1].node_data.name, "Charlie");
834 assert!((nbrs[1].edge_data.weight - 5.0).abs() < 1e-12);
835 }
836
837 #[test]
838 fn test_attributed_neighbors_unknown_node() {
839 let g = make_graph();
840 assert!(attributed_neighbors(NodeId(99), &g).is_err());
841 }
842
843 // ------------------------------------------------------------------
844 // Dijkstra
845 // ------------------------------------------------------------------
846
847 #[test]
848 fn test_dijkstra_simple() {
849 let g = make_graph();
850 // Alice=0, Bob=1, Charlie=2
851 // 0→1 (1.0), 1→2 (2.0), 0→2 (5.0)
852 let dist = dijkstra_attributed(&g, NodeId(0), |r| r.weight).unwrap();
853 assert!((dist[&NodeId(0)] - 0.0).abs() < 1e-12);
854 assert!((dist[&NodeId(1)] - 1.0).abs() < 1e-12);
855 assert!((dist[&NodeId(2)] - 3.0).abs() < 1e-12); // 1+2 beats direct 5
856 }
857
858 #[test]
859 fn test_dijkstra_unreachable() {
860 let mut g = AttributedGraph::<(), f64>::new();
861 g.add_node(());
862 g.add_node(());
863 // No edges; node 1 is unreachable from node 0
864 let dist = dijkstra_attributed(&g, NodeId(0), |e| *e).unwrap();
865 assert!(dist.contains_key(&NodeId(0)));
866 assert!(!dist.contains_key(&NodeId(1)));
867 }
868
869 #[test]
870 fn test_dijkstra_invalid_source() {
871 let g = make_graph();
872 assert!(dijkstra_attributed(&g, NodeId(99), |r| r.weight).is_err());
873 }
874
875 #[test]
876 fn test_dijkstra_single_node() {
877 let mut g = AttributedGraph::<(), ()>::new();
878 g.add_node(());
879 let dist = dijkstra_attributed(&g, NodeId(0), |_| 1.0).unwrap();
880 assert_eq!(dist.len(), 1);
881 assert_eq!(dist[&NodeId(0)], 0.0);
882 }
883
884 // ------------------------------------------------------------------
885 // Utility functions
886 // ------------------------------------------------------------------
887
888 #[test]
889 fn test_in_out_degrees() {
890 let g = make_graph();
891 let out = out_degrees(&g);
892 // Alice: 2, Bob: 1, Charlie: 0
893 assert_eq!(out[0], 2);
894 assert_eq!(out[1], 1);
895 assert_eq!(out[2], 0);
896
897 let inn = in_degrees(&g);
898 // Alice: 0, Bob: 1, Charlie: 2
899 assert_eq!(inn[0], 0);
900 assert_eq!(inn[1], 1);
901 assert_eq!(inn[2], 2);
902 }
903
904 #[test]
905 fn test_filter_nodes() {
906 let g = make_graph();
907 let young = filter_nodes(&g, |p| p.age < 31);
908 // Alice(30), Bob(25)
909 assert_eq!(young.len(), 2);
910 }
911
912 #[test]
913 fn test_nodes_iterator() {
914 let g = make_graph();
915 let names: Vec<&str> = g.nodes().map(|(_, p)| p.name.as_str()).collect();
916 assert_eq!(names, vec!["Alice", "Bob", "Charlie"]);
917 }
918
919 #[test]
920 fn test_edges_iterator() {
921 let g = make_graph();
922 let edges: Vec<(NodeId, NodeId, f64)> =
923 g.edges_iter().map(|(s, d, e)| (s, d, e.weight)).collect();
924 assert_eq!(edges.len(), 3);
925 }
926}